Esta página todavía no está en español. Lo que sigue es el original en inglés. Abrir la versión en inglés →
Edda Sprint Winners: The Code Review

Over the course of the Edda Labs sprint, participants raced through tasks, stacked up points, and, most importantly, shipped real code on Midnight using our starter template. As promised, the top accumulators earned a full code review of what they built.
This post is that review. Three repositories, two winners:
| Place | Winner | Project(s) |
|---|---|---|
| 🥇 1st | tminus1sec | bahamas-2026-private-poll, midnight-allowlist-token |
| 🥉 3rd | Spy | nightforce-intelligence |
The bar we reviewed against
Everyone in the sprint started from the midnight-starter-template: a pnpm + Turbo monorepo with a counter contract (counter-contract), an interactive CLI (counter-cli), and a React frontend (frontend-vite-react).
Since the sprint ended, we've been building its successor, the modular-starter, and it's the reference we point to throughout this review. The headline changes worth knowing:
- One deployed contract, many Compact modules. Features live as independent Compact
modules (src/modules/<feature>/<Feature>.compact), imported with an explicitprefixand composed into a single entrypoint contract. The thin contract-level wrapper circuits are where validation and access control belong. - An embedded passkey wallet. Alongside the Lace extension flow, the frontend ships its own wallet: an HD seed derived from a WebAuthn passkey (the PRF extension), registered on
window.midnightso the existing dApp-connector code path works unchanged. Users can try a Midnight dApp with nothing installed: no extension, no seed phrase to copy down.
With that bar in mind, let's look at what our winners built.
🥇 First place: tminus1sec
tminus1sec took the top spot with two submissions, and they're a great pair, because each one shows a different kind of discipline: one is a full-stack dApp with real privacy engineering, the other is a small, surgically scoped contract library with excellent documentation.
Bahamas 2026 Private Poll
1st lugar · tminus1sec
bahamas-2026-private-poll
- Compact
- React 19
- Vite 6
- TanStack Router
- Tailwind 4
- pnpm + Turbo
A privacy-themed community straw poll for the May 2026 Bahamas general election. Voters pick a constituency and cast a ballot for a candidate; each vote is anonymous, and each secret key can only vote once. The public tallies update live, per candidate.
The contract: a real nullifier scheme, not a renamed counter
The heart of the project is a genuinely privacy-relevant construction. The voter proves control of a secret key (a private witness), and the contract records only a nullifier, a one-way hash of that key. Voting twice reproduces the same nullifier, which is already spent, so the second vote is rejected. The key never touches the ledger:
// the voter's secret identity key (never disclosed)
witness localSecretKey(): Bytes<32>;
export circuit vote(candidate: Uint<16>): [] {
const nf = persistentHash<Vector<2, Bytes<32>>>(
[pad(32, "bahamas-poll:nullifier:v1"), localSecretKey()]
);
assert(!spentNullifiers.member(disclose(nf)), "You have already voted in this poll");
spentNullifiers.insert(disclose(nf), true);
const id = disclose(candidate);
if (!votes.member(id)) {
votes.insert(id, 0);
}
const current = votes.lookup(id);
votes.insert(id, (current + 1) as Uint<64>);
}Three things here deserve a call-out, because they're exactly what we hope to see in Midnight contracts:
- Correct
disclose()discipline. Only the nullifier and the candidate id, the two values that must reach the public ledger, are disclosed. The raw witness never is. - The right hash for the job.
persistentHash(nottransientHash) is the correct choice for a value persisted on-chain across transactions. - A domain-separated, versioned nullifier. The
"bahamas-poll:nullifier:v1"tag scopes the nullifier to this poll. Without it, the same secret key would produce an identical value in any other contract that hashes a key the same way. Remarkably, the in-source comment documents that this actually happened between this poll and the same author'smidnight-allowlist-tokenbefore both were fixed. Finding a real cross-contract nullifier collision during a sprint and fixing it with versioned domain tags is the single best story in this whole review.
Recommendation: let the ledger do the counting
The main design improvement we'd suggest is in the tally update. The lookup → insert sequence is a read-modify-write: the transaction reads the current tally and writes back current + 1, which couples it to the exact ledger state it was proven against. Under concurrent voting, two people voting for the same candidate against the same observed state, those transactions contend, and for a live poll that's the difference between a demo and production.
Compact has a purpose-built answer: Counter works as a Map value, and its increment is an atomic delta rather than a read-and-replace. We verified this compiles and executes correctly (independent per-key counters, accumulating across calls):
export ledger votes: Map<Uint<16>, Counter>;
export circuit vote(candidate: Uint<16>): [] {
// ...nullifier check as before...
const id = disclose(candidate);
if (!votes.member(id)) {
votes.insert(id, default<Counter>);
}
votes.lookup(id).increment(1);
}Two smaller state-design refinements in the same spirit:
spentNullifiersshould be aSet<Bytes<32>>, not aMap<Bytes<32>, Boolean>whose value is alwaystrue. Samemember/insertAPI, cheaper and clearer intent (spentNullifiers.insert(disclose(nf));).- Validate the candidate id.
Uint<16>accepts 0–65535, but only ids 0–14 map to real candidates. A one-lineassert(candidate < 15, "unknown candidate")prevents junk tallies and unbounded ledger growth. In the modular-starter pattern, the thin contract-level wrapper circuit is exactly where this validation belongs.
And one lifecycle thought for a v2: the poll has no constructor, no owner, and no open/close window; it's permanently open by design. Fine for a straw poll, but an export sealed ledger owner plus a close circuit would make it a complete election primitive.
Tests, tooling, and the frontend
The contract test suite is the kind we like: a multi-identity simulator (sim.as("p1") / sim.as("p2") with per-user private state) driving five focused tests, including the double-vote rejection and an assertion that the rejected attempt left the tally untouched. Keys come from crypto.getRandomValues, both in tests and in the browser, where the secret key is generated once, persisted through the private-state provider, and never leaves the device.
The frontend is genuinely custom work: a ballot UI with constituency selection, per-candidate result bars with bigint-safe percentage math, and a four-stage transaction tracker (Prove → Sign → Submit → Confirm). The election data module cleanly maps on-chain ids to (constituency, party, candidate) and labels itself a demonstration poll.
Lo que nos encantó
- Real privacy engineering: domain-separated, versioned nullifier with correct disclose() placement
- Exceptionally honest privacy documentation: disclaims what it doesn't do and names the fix
- Found and fixed an actual cross-contract nullifier collision during the sprint
- Multi-identity simulator tests including double-vote rejection
- Purpose-built ballot UI with staged transaction progress
Oportunidades de mejora
- Use Map<Uint<16>, Counter> + increment to avoid read-modify-write contention
- Use Set<Bytes<32>> for spent nullifiers; validate candidate id bounds
Midnight Allowlist Token
1st lugar · tminus1sec
midnight-allowlist-token
- Compact
- TypeScript
- Vitest
- npm workspaces + Turbo
The second submission takes the opposite shape. No frontend, no CLI, just one carefully written contract, a simulator, nine tests, and documentation that's better than most production repos. It's a permissioned (KYC-style) unshielded token: an owner administers an allowlist, and tokens can only be minted to or received by allowlisted accounts.
The contract: small, idiomatic, and honest about its privacy model
Since Compact has no implicit msg.sender, callers prove who they are by knowing a secret key supplied through a witness, with the on-chain identity being a commitment:
export sealed ledger owner: Bytes<32>;
export pure circuit accountId(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "allowlist:accountid:v1"), sk]);
}
circuit assertOwner(): [] {
assert(callerId() == owner, "AllowlistToken: caller is not the owner");
}
constructor() {
const deployer = callerId();
owner = disclose(deployer);
allowlist.insert(disclose(deployer), true);
}The best-practice checklist basically writes itself here:
sealedused correctly: the owner is set once in the constructor and can never be reassigned.- Domain-separated identity derivation (
"allowlist:accountid:v1"), added in a dedicated commit after the cross-contract collision discovery mentioned above. - An explicit overflow guard on
mint(MAX_UINT64 - totalSupply >= amount), the kind of check that's easy to skip in a sprint. - Deliberate
disclose()placement, including the deliberate absence of it inassertOwner(): asserting on a witness-derived value doesn't publish it, and the comments explain exactly that. - The privacy model is stated, not implied. The header says plainly: this is an unshielded token; balances, the allowlist, and account commitments are public; only the secret key behind each identity is private; value/owner privacy would need a Zswap design this contract intentionally doesn't attempt.
One design choice reviewers will notice: transfer gates the recipient on the allowlist but not the sender, so a de-listed holder can still send tokens out. The project's own docs.md lists fixing this as a reader exercise, so it's a documented scope decision, not an oversight. But for anything approaching a real regulated asset, we'd gate both ends.
Tests and documentation
The nine simulator tests cover every revert path by exact assertion message (non-owner rejection, mint to non-allowlisted, insufficient balance, post-removal mint) alongside the happy paths, with an invariant check that transfers don't change total supply. The simulator itself exposes the contract's pure circuit accountId via pureCircuits, so tests compute the same commitment off-chain that the contract derives on-chain, a small version of the cross-verification pattern we bake into the modular-starter's token tests.
Lo que nos encantó
- Small, idiomatic contract: sealed owner, domain-separated commitments, overflow guard
- Nine simulator tests covering every revert path by exact message
- Off-chain/on-chain cross-verification of the accountId derivation
- docs.md is education-grade, the best documentation of the sprint
- Clean repo: correct Turbo graph, gitignored build output, pinned toolchain, honest scope
Oportunidades de mejora
- Gate the sender on the allowlist (or promote the docs.md exercise into the contract)
- Package it as a reusable module: it's one prefix import away
🥉 Third place: Spy
Nightforce Intelligence
3rd lugar · Spy
nightforce-intelligence
- React 19
- Vite 6
- TanStack Router
- Tailwind 4
- shadcn/ui
- pnpm + Turbo
Nightforce Intelligence is a spy-themed "private AI operations center": an AI chat codenamed Spy, a private preferences vault, a mission board with XP, and a reputation page with badge-based proof-of-contribution. For a developer at the start of her Midnight journey, what Spy shipped is genuinely impressive. A complete product vision, designed and built end to end. The frontend craft is real: a coherent dark monospace theme carried across five pages, consistent wallet gating with connect prompts, sensible loading and empty states, and a counter page with a well-built four-stage transaction progress indicator driven off the provider's flow messages. She also got the whole starter monorepo running, deployed the app, and wrote up her deployment procedure, none of which is trivial the first time through.
That's the hard part of a dApp that most people never finish: knowing what you want to build and making it feel like a product. What's left is the fun part: moving the features from the browser onto the chain. Today the contract is still the starter counter, and the vault, missions, and reputation pages run on local state. Which means the design work is done and each feature is now waiting for its contract.
The path to bring Nightforce on-chain
The best news in this review: Nightforce's feature set maps beautifully onto Compact, and none of it requires anything the sprint hasn't already demonstrated. The modular-starter's module pattern is the natural way to build it: one module per feature, composed into a single deployed contract. The wiring pattern she needs already exists in her own repo (the counter page's SDK and providers are exactly the template to copy for each new circuit). A build order we'd suggest, smallest first:
- Start with Missions, the friendliest first contract. A
Map<Bytes<32>, Set<Uint<16>>>of completed mission ids per account commitment, with a completion circuit that validates the mission id. One ledger declaration, one circuit, and suddenly XP survives a page reload because it lives on-chain. - Then the Vault, the flagship private-state use case. Keep preferences in witness-backed private state on the device; put only commitments on-chain (
persistentHashwith a domain tag like"nightforce:vault:v1"). That's real Midnight-style privacy: the data never leaves the user, but facts about it can be proven later. - Then Reputation, the real version of the badge proof. Enroll contributors into a
MerkleTreeand let a member prove membership without revealing which member she is. That's an actual ZK proof-of-contributor, and it's closer than it sounds: tminus1sec's nullifier work in this same sprint is most of the recipe, in public code she can read.
Alongside the contract work, a few quick wins would let the existing app shine more: a Nightforce README (the repo currently shows the starter's, and her project deserves its own front door), linking the counter page in the nav so visitors can find the part that already talks to the chain, and letting the deploy build compile the contract instead of the temporary stub so the published site can go on-chain too. Each is an afternoon, not a project.
Lo que nos encantó
- The most polished, coherent UI of the sprint: theme, states, and navigation feel finished
- A complete product vision, designed end to end: the hardest part of any dApp
- Genuine custom work on the transaction progress flow for the counter page
- Consistent wallet gating across pages
- Got the full starter monorepo running, deployed, and documented
Oportunidades de mejora
- Bring the features on-chain one module at a time: missions first, then vault, then reputation
- Add a 'demo mode' badge to simulated features until their contracts land
Cross-cutting lessons for the next sprint
Reviewing three repos side by side, the same themes kept surfacing. Take these as the checklist for next time:
- Rename the scaffolding on day one. All three repos still say "counter" somewhere a judge will look: package names, routes, banners, READMEs. A thirty-minute rename makes a project read as yours.
- Prefer commutative ledger operations for shared state. When multiple users write the same state concurrently,
Counter.incrementcomposes;lookup-then-insertcontends. Reach forCounter(including as aMapvalue withdefault<Counter>) andSetbefore hand-rolling read-modify-write. - Domain-separate every hash. The sprint proved why. The same author's two contracts briefly derived identical nullifiers from one key. Versioned domain tags (
"myapp:purpose:v1") are two tokens of code and the difference between isolated and linkable identities. - Validate at the boundary. Circuit arguments are attacker-controlled inputs. Bounds-check ids, gate both ends of a transfer, and put the checks in the contract-level wrapper circuits where they belong.
Congratulations 🎉
To tminus1sec: two submissions that each model a different virtue: real privacy engineering with rare intellectual honesty, and a small library with documentation that teaches. To Spy: a first Midnight project with real product vision and the best-looking UI of the sprint. The path above is yours, one module at a time, and we can't wait to see Nightforce go on-chain.
Both winners (and everyone else building on Midnight) should point their next project at the modular-starter: composable Compact modules, an embedded passkey wallet, an SDK layer, and reproducible deployments out of the box. We built it from everything this sprint taught us.
Until the next one: keep it private, keep it honest. 🌙
Reviewing a Compact contract of your own? Habla con nosotros →