Inspiration
If your company is big enough, it's legally required to have an internal whistleblowing channel. The EU Whistleblower Directive mandates it and plenty of other countries have their own version.
Nearly nobody uses them.
The reason is obvious once you say it out loud: the employer runs the channel. Reporting your employer means trusting your employer not to go looking for who filed it. Some companies do the honest thing. You have no way to know if yours is one of them.
The usual fix is an anonymous tip line, which trades one problem for another. If anyone can submit, then a competitor can, a bot can, a former employee with a grudge can. So HR treats every report as unverifiable and the serious ones get buried with the noise.
We kept circling the same question: can a report be anonymous and still be provably from a real employee? On a regular backend, no — someone always holds the mapping. On a regular blockchain, no — proving membership means publishing something that points back at you. Midnight was the first stack where the answer wasn't "sort of."
What it does
Three roles.
The employer enrolls employees by publishing a commitment: a hash of a secret the employee's own machine generated. The employer never sees the secret. It can add people to the channel, and that is the entire extent of its power.
An employee files a report to a public board. The contract checks a zero-knowledge proof that they hold a credential in the enrolled set, without learning which one. A nullifier caps each credential at one report per round so nobody can flood the board. Reports land with a "verified employee" badge and no name.
Later — a day later, a year later, never, entirely their call — the reporter can prove they wrote a specific report, to a specific person. The Anyone can check all of this from public chain data. The investigator console shows the raw values it reasoned from, on purpose.
Deployed on Midnight Preview
Contract: 609798fdcc41a37fd2166bdf275861353444bc8f1a62a7614723ba9d16dadacc
Live contract, not a local simulation. The enrolled set, the spent nullifiers, every report and every disclosure in our demo are readable from that address by anyone. deployment.preview.json in the repo has the deployment transaction.
How we built it
Contract (Compact). Enrollment commitments go into a HistoricMerkleTree. Membership is proven against historic roots, not just the current one — otherwise an enrollment landing while someone's proof is in flight silently invalidates that proof. Nullifiers handle the one-per-round cap. The proveAuthorship circuit reads the report's stored round rather than the current round, which is the difference between disclosure working on old reports and quietly not working at all.
Backend. Three separate Node agents, one per role. Not one server with three views. Each has its own wallet and its own encrypted local state, and the employee's secret is generated inside the employee's agent and never leaves it. A single shared backend could deanonymize everyone, and then the whole pitch falls apart.
Frontend. React, four views. The investigator's verification screen explains its reasoning in plain English and then shows the on-chain values underneath, because "trust our UI" is the wrong thing to ask for a tool like this.
Infra. The Compact compiler doesn't shieveryone hand-configure WSL. Local proof server in Docker. Real deploy to Preview.
Challenges we ran into
The Compact compiler being Linux/macOS only. The dev container solved it, cost us an hour.
A dependency resolving twice. @midnight-ntwrk/onchain-runtime-v3 ended up in the tree at two versions, and it's wasm-backed, so StateValue is a different class per copy. Objects built by one copy failed instanceof against the other. Every offline test passed. It only broke on our first real call to the deployed contract, which is the worst possible moment to find something like that. Pinning the version fixed it.
DUST. NIGHT doesn't pay for transactions — DUST does, and DUST comes from registering NIGHT and then waiting for it to accrue. Our deploy failed with InsufficientFunds several times before that clicked.
Accomplishments that we're proud of
What we learned
Most of what "privacy" means in a system like this comes down to one question: where is the secret allowed to live? Once we answered that honestly, the three-agent architecture stopped being a design choice and became the only option.
Also: a bug that passes every test and only appears when real infrastructure meets real infrastructure will find you at the worst possible hour.
What's next for Canary
Third-party enrollment, so the employer isn't the only gatekeeper of who counts as an employee. Encrypting disclosure payloads to the investigator instead of publishing them in the clear. A PreProd hardening pass.
Running it yourself
Setup is in the repo README: prerequisites, the Docker dev container (Windows included), funding a Preview wallet from the faucet, registering NIGHT for DUST, compiling, deploying, and bringing up the three agents plus the frontend. docs/threat-model.md lays out exactly what the employer, a chain observer, and an investigator can and cannot see.
Built With
- compact
- docker
- express.js
- midnight
- node.js
- pnpm
- react
- typescript
- vite
- zero-knowledge
Log in or sign up for Devpost to join the conversation.