Project name: EchoCert on Midnight

Elevator pitch: Prove one field of a diploma. Reveal nothing else. Leave nothing behind that links two proofs to the same person.

Track: Integrate Midnight to Upgrade an Existing App


Inspiration

EchoCert is my credential registry on Cardano — a real product, live since April. It does its job: a school anchors a diploma, anyone can verify it. But I built it with a flaw I could not fix on a transparent chain: to prove one thing about yourself you must publish everything. Name, school, year, anchor — public, forever, linkable.

Midnight is the first chain where I could fix that without giving up the part that works: anyone can still verify.

The story

Midnight University, two applicants. Chuck has a real diploma, already public on EchoCert on Cardano — verifiable by anyone, and readable by anyone. On Midnight he redacts four fields and proves one; the university learns the degree and nothing else, and three universities cannot tell they saw the same applicant. Charles has no diploma, so he forges one; his own device refuses to prove it, in twenty milliseconds, before anything is sent. Chuck is my nickname. Both applicants are me: I built the public registry, and then the private proof.

What it does

The holder keeps the credential on their own device. The chain holds only a commitment, hashed again before it is stored. To convince a university, the holder proves in zero knowledge that their credential is in the registry and disclosing exactly one field — DEGREE, say. The university learns "the diploma is real, the degree is BSc Computer Science" and nothing else.

Three properties, all demonstrated live in the demo:

  • Selective disclosure — click fields to redact them; what is blacked out never leaves the device, not even encrypted.
  • Local refusal of forgeries — a forged diploma fails on the forger's own machine in ~20 ms, before any network call. There is nothing to submit.
  • Unlinkability — the same holder proves to three universities; the three transactions share nothing that two different holders' proofs don't also share. This is measured by an experiment in the repo, not asserted.

The BEFORE half is real too: the demo credential is anchored to an actual EchoCert record minted on Cardano mainnet on 2026-04-11 — the anchor field is that record's asset name, and the demo links to it so you can check.

How I built it

  • Compact contract (6 circuits): commitment registry on a HistoricMerkleTree — historic roots mean issuing a new credential doesn't invalidate paths already in holders' hands. The one line that carries the security: assert(path.leaf == commitment), without which a forger could pair someone else's genuine Merkle path with an invented credential.
  • TypeScript witnesses with nullable secrets that throw locally rather than fall back to defaults (a zero-byte issuer secret would be forgeable by anyone).
  • Real pipeline: local proof server (PLONK), deploy/issue/prove on a Midnight devnet and on public preprod, wallet-less chain reads through the public indexer.
  • The demo is vanilla HTML/CSS/JS in my design system, with a LIVE mode (a local prover service generates real proofs behind the page) and an honest REPLAY mode that replays measured timings and says so. The prover streams its phases, so the page shows the exact moment the proof exists (1–4.5 s, different every time), condenses the redacted fields into the only value that leaves the device — the SHA-256 of the chosen field, 32 bytes — and, once the transaction lands, fetches it back from the public indexer: hash, block, contract, real size in bytes, and the raw response beside the verdict.
  • Built by pair-programming with Claude Code, disclosed in the repo.

Challenges

  • The zero-fee trap. A circuit that only reads state computes a zero fee on an idle chain; the wallet then builds an empty DustActions and the ledger rejects it (Invalid Transaction: Custom error: 117 — NotNormalized). Deploy and issue write state, so only the proof transactions failed — a uniquely confusing asymmetry. One line fixes it: a small additionalFeeOverhead.
  • I nearly shipped a false claim. My demo said the three proofs "share no identifier". When I actually diffed the raw transaction bytes, they shared a 41-byte run — which turned out to be the Merkle root at proof time, not an identity. The claim survived, narrower and now backed by a repeatable experiment (contract/e2e/unlinkability.ts). The commit history keeps the wrong turn.
  • Preprod cold sync vs. 16 GB of RAM. Syncing a wallet from genesis OOM-crashed Node at 4, 8 and 10 GB heaps. I built a resumable sync that checkpoints all three wallet states to disk every 40 s and restores after every crash — progress is monotone even if every attempt dies. It got there — after two kernel panics that turned out to be Docker Desktop's VM, not the sync (the containers moved to OrbStack). Deploy, issue and proveDegree all landed on public preprod, 7/7 checks, and the fee fix above turned out to be the root cause of a failure I had spent two days on before the event. The finding is filed upstream as midnight-wallet#700.

Accomplishments

17/17 local checks including the forgery attack; 7/7 end-to-end checks on a devnet with real proofs; an unlinkability claim that is measured; a demo where the eligibility line, the privacy grammar (○ LOCAL / ● DISCLOSED), four languages and the sound design are all part of the product.

What I learned

That "compiles" and even "passes tests" mean nothing until you grep the raw transaction bytes. Taint analysis is not a privacy audit — the compiler was happy long before the chain stopped leaking.

Business value

EchoCert is not a concept: it is a shipped credential registry on Cardano, live since April 2026, and the anchor in this demo is a real record minted there on 2026-04-11. Every issuer and holder already on EchoCert gets the Midnight path as an upgrade, not a migration — same credential, same anchor, verification that discloses one field — and the entry point ("Prove privately on Midnight") is already live on the product page. The registry is the business; Midnight is what lets it serve the verifications people could not afford to make in public: admissions, employment, age, licensing.

What's next

Range proofs on issuedYear ("graduated before 2027" without the year), more issuers, and folding this back into the EchoCert product line as the private verification path next to the public registry.


Built with: Compact 0.31.1 · midnight-js 4.1.1 · proof server 8.1.0 · React-free vanilla JS · IBM Plex Mono · Kenney CC0 audio

Links: video https://youtu.be/4r4gVs97vUo · repo https://github.com/EchoForge-Dev/EchoCert_Midnight_Demo · live demo https://m.echoforgeef.com/echocert/ · the Cardano anchor: cardanoscan.io/token/32fd4d60…bed978 · preprod contract 4719d2f6ebcddbda079ac07ec1cc7ea4019471ba254ca1846461c8e204d0769b

Built With

Share this project:

Updates

posted an update —

Lace note — measured it today: on Preprod the initial sync dies at ~99 % every cycle (the extension hits ~4 GB, relocks or crashes, then resumes from an old checkpoint), so a first-time wallet never finishes syncing. That's the "not responding". Filed as input-output-hk/lace#2257. Workaround for now: a headless SDK wallet with a checkpoint/restore loop.

Log in or sign up for Devpost to join the conversation.

posted an update —

Midnight Bug Report: Every Midnight transaction pays a fee, and the wallet estimates it from how much the transaction does. Our "prove my degree" transaction only reads the registry and changes nothing, so on a quiet chain the estimate came out as exactly zero. A zero-fee transaction is invalid — but instead of saying so, the wallet either submitted it and got a cryptic rejection (test node) or hung for twenty minutes and crashed inside itself (public network). Transactions that write data always cost something, so only the proof step failed, which made it look like a bug in our privacy logic. The fix is one line: tell the wallet to always add a small fee margin.

Log in or sign up for Devpost to join the conversation.

Submission history