DEV Community

Cover image for The receipt should come from the person who received it
Yash Kumar Saini
Yash Kumar Saini Subscriber

Posted on

The receipt should come from the person who received it

DEV Weekend Challenge: Generosity Edition Submission 💜

This is a submission for Weekend Challenge: Generosity Edition

What I Built

Every donation platform proves the same single fact: money left your account. The fact you actually care about is the second one, that it reached a person, and the receipt for the first is issued by the organisation holding the money. The thing you're trusting and the thing producing the evidence are the same thing.

Cairn changes who issues the receipt.

You lock SOL in an escrow no intermediary can open. The only thing that moves it is a signature from the recipient's own wallet, and that transaction carries the hash of a recording where they say, in their own words, what they received. The receipt comes from the person at the end of the chain, and the chain welds it to the transfer.

Cairn — proof of arrival for charitable giving

Three commands, and the third one is the whole project:

cairn give     --to <them> --amount 0.05 --for "one term of school fees"
cairn receive  --escrow <pubkey> --audio receipt.wav   # they run this
cairn verify   <signature>                             # anyone runs this
Enter fullscreen mode Exit fullscreen mode

cairn verify re-downloads the recording, re-hashes the bytes it gets back, rebuilds the receipt from scratch and compares the result against what's on chain. It doesn't ask my server to be honest. With cairn hash it doesn't need my server to exist.

A cairn is a stack of stones travellers raise along a route. Each person who passes adds one. It proves somebody came through, it guides whoever comes next, and it works with nobody owning it and nobody supervising it. Every clause maps onto something the software does, which is the only reason the name stuck.

Demo

docs/COMMANDS.md walks the whole loop with real captured terminal output at every step — funding an escrow, recording a receipt, releasing, and verifying.

The command that matters:

cairn verify, passing

Those two hashes were produced by different code paths, minutes apart. The same command against a recording whose bytes have been altered:

cairn verify, mismatch

Two visibly different hashes and a non-zero exit code, so it can gate a script.

Code

Cairn — proof of arrival for charitable giving

A weekend build. Lock some SOL for someone; they only get it by signing a transaction that carries the hash of a voice recording saying what they received.

Devnet. Not audited. About 4,500 lines of Rust, of which 400 are the test suite, and no frontend at all.

cairn give     --to <them> --amount 0.05 --for "one term of school fees"
cairn receive  --escrow <pubkey> --audio receipt.wav     # they run this
cairn verify   <signature>                               # anyone runs this
Enter fullscreen mode Exit fullscreen mode
A terminal session: funds are locked against a description, the recipient records a receipt and releases them, verification passes, the stored recording is altered, and verification then fails.

A real session against a local validator — the escrow is created on chain, the receipt is signed by the recipient's key, and the failure at the end is a genuine hash mismatch after the stored recording is altered. Also available as mp4, or replay it yourself with asciinema play assets/demo.cast.


The itch

Donation platforms verify that money left you. They don't verify…

A weekend build: roughly 4,500 lines of Rust, 400 of them the test suite. MIT.

cairn/
├── programs/cairn/      custody, authorization, state machine
├── crates/
│   ├── cairn-core/      canonical receipt, hashing, seeds, account layout
│   ├── cairn-api/       blob store, index, verifier. Holds no keys.
│   └── cairn-cli/       the client. All of it.
└── docs/
Enter fullscreen mode Exit fullscreen mode

How I Built It

The axis everything turns on

Two things here look similar and are not, and the entire design is the consequence of separating them.

Authorization is an ed25519 signature from the recipient key the donor names at creation. The program enforces it. No server key, operator key or admin override substitutes for it.

Testimony is the recording. Human evidence, hashed and committed on chain so it's tamper-evident and bound to exactly one transaction signature.

The recording authorizes nothing. Audio used as an authentication factor is walked through by replay or a decent TTS model, so Cairn treats it as attached evidence with guaranteed integrity instead. That's a much smaller claim, and it's one the software can actually keep.

Cairn architecture

Note the asymmetry. The recipient's way out is a signature; the donor's way back is a deadline. There's no third arrow and nobody standing in the middle.

One hash, and everything checks against it

Strip Cairn down and a single sentence is left: two independently computed hashes agree. Everything below exists to make that sentence hard to fake and easy to check.

It starts with the encoding.

receipt_hash = sha256(
    b"cairn:receipt:v1" ‖ escrow(32) ‖ audio_sha256(32) ‖ transcript ‖ locale(8) ‖ recorded_at(8)
)
Enter fullscreen mode Exit fullscreen mode

locale is a fixed 8 bytes rather than a string, and that's the load-bearing choice. Every field except the transcript is fixed width, so for a preimage of length L the transcript is always exactly L - 96 bytes. The encoding is injective with no length prefixes anywhere: no two distinct receipts concatenate to identical bytes. Add a second variable-length field and that evaporates — ("ab", "c") and ("a", "bc") land on the same preimage, and two different receipts share one hash. A test named transcript_and_locale_do_not_smear exists purely to fail if someone later decides locale should be a String. Transcripts are NFC-normalised, trimmed and whitespace-collapsed first, so the same recording hashes the same regardless of which machine composed the receipt.

Then there's the question of how many implementations of that function exist, and the answer is one. cairn-core compiles to both sbf-solana-solana and the host, so the on-chain program, the CLI and the API server all call the same receipt_hash(). Not kept in sync, not mirrored, not generated from a shared schema — the same function, compiled twice. The account layout works the same way: Escrow is defined once in cairn-core and the program wraps it in a newtype that Anchor's traits hang off, so the bytes the program writes are the bytes the indexer reads, with a test asserting that Anchor's generated discriminator matches the constant the decoder uses.

That count is also why the client is a CLI. A browser would need its own receipt_hash(), which means hand-porting the normalisation into TypeScript, and JavaScript's \s and Rust's char::is_whitespace disagree about U+FEFF — a byte-order mark in a transcript gives you a browser reporting a valid receipt and a chain reporting a fake one. That character is patchable; the shape of it isn't. Any second implementation is a second answer, and an interface whose job is to display "these match" while running different code from the thing doing the matching argues against itself.

With one function to check against, three commands do the checking rather than taking anything on faith. cairn show re-hashes the description client-side against the on-chain need_hash, so the API can serve whatever text it likes and it doesn't matter. cairn receive rebuilds the receipt from the audio on the recipient's own disk and refuses to sign if the server's hash doesn't cover it. cairn hash touches no network, needs no key and reads no database — compute a hash with the wifi off and compare it against a block explorer.

Which leaves the server holding nothing worth stealing: no keypair, no signing path, no instruction it could call to move a lamport.

Speech, and why the transcript is a convenience

Transcription runs through ElevenLabs Scribe. The recipient speaks whatever language they actually speak, and the transcript joins the canonical receipt alongside the locale tag.

It's wired as best-effort enrichment rather than a dependency. The hash covers the audio bytes either way, so a receipt is valid with or without a transcript — the audio is the evidence, and the transcript is a convenience for whoever reads it later. Depending on a third party that lightly felt like the right call inside something whose whole argument is about not needing to trust third parties.

What Cairn proves

One sentence, precisely: that a specific recording, unaltered, was committed by the holder of a specific key in the same instruction that moved specific funds. There's no window in which the money has moved without a receipt, or a receipt exists without the money.

Every design decision above exists to make that sentence checkable by a stranger who has no access to me, my server, or my database. Every state transition has a happy-path test in LiteSVM, and every guard has a negative test asserting its specific error code. The one that carries the trust model is only_the_named_recipient_can_release.

Prize Categories

Best Use of Solana. The escrow program isn't a payment rail bolted onto the side of the idea — it's the only reason the idea works. A receipt that anyone with database access can remove is a receipt back under the control of the party you were trying not to have to trust, which is the exact problem the project starts from. Putting it on Solana moves custody into a program-owned vault PDA with deterministic seeds and no operator key, makes the recipient's ed25519 signature the sole authorization primitive, and makes the receipt append-only by construction rather than by policy. The program is the trust model. Everything off-chain is a convenience wrapped around it, and it says so by holding no keys.

Best Use of ElevenLabs. Scribe closes the language gap that sits between a donor and a recipient by default. The audio is the evidence, but audio in a language you don't read is evidence you can't check, and a transcript makes it legible to someone on the other side of that gap. The locale tag travels inside the hash preimage alongside the transcript, so the language a receipt was given in is part of what gets committed — not metadata bolted on afterwards, but a field the signature covers.

It was a good challenge that stretched me to make use of solana and AI together in a interesting manner. Interesting part was the idea of getting the scribe using eleven labs. Overall a good exercise to learn something new.


Yash K Saini — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.

Top comments (1)

Collapse
 
neovim-dev profile image
Tony Stark

By far a very interesting use case of eleven labs being integrated into a solana application.