[ 11 / 11 ] · Freshman Track

Lesson 11: Implement Cadence, Single-Slot Version

12 minutes200 XP

Embark on your journey to building production grade apps.

This lesson is about a brand new protocol: Cadence, from Category Labs.

Cadence is a consensus protocol. A consensus protocol is the part of a blockchain that deals with validators:

  1. How should a validator propose a block?
  2. How should all of Monad's validators agree on which blocks are valid?
  3. and more.

After completing this lesson, you will understand how a simplified version cadence works! You will also vibecode a simplified version of cadence on your computer. We encourage you to share your builds on X (Twitter)!

What Cadence Actually Is

Let me explain the big picture before you build, because a couple of the ideas are subtle and worth understanding first:

A blockchain needs its validators to agree on the order of blocks even though some of them may be faulty, slow, or actively malicious. Cadence is a Byzantine-fault-tolerant (BFT) consensus protocol: Byzantine-fault-tolerant means it keeps working correctly as long as no more than a 33% of validators misbehave.

But Cadence also solves a second problem at the same time: MEV / front-running:

The problem: When transactions sit in a public mempool waiting to be included, anybody can see your transactions and exploit them. For example, they can front-run your trades, sandwich your swaps, or reorder transactions for profit.

The fix is an encrypted mempool: transactions are encrypted until the moment their order is locked in. If nobody can read a transaction while its position is being decided, nobody can front-run it. Cadence bakes this directly into its consensus.

So one "slot" of Cadence does two jobs at once:

  1. Agree on which proposals go in the block (BFT consensus).
  2. Reveal the transactions only as that agreement forms.

The elegance is in how those two jobs are woven together — you'll see it when you build the voting step.

What does it mean that Cadence is Byzantine-fault-tolerant (BFT)?

Can you see transactions in an encrypted mempool?

A Quick Vocabulary Refresher

Here are a few helpful terms to remember:

Validator. A node that participates in consensus.

Slot. The timeframe in which one block is proposed and agreed on.

Deadline. Validators have synchronized clocks, so all 200+ validators agree on the time! A deadline is the moment by which a slot's proposals must arrive.

Proposer. A validator that gets to suggest transactions for a slot. Most blockchains pick one proposer per block. Cadence picks several. Each submits its own list of transactions, and the slot's block combines all the lists into one.

Quorum. A quorum is the minimum number of votes needed to make a decision final. In Cadence that means more than two thirds of the validators voting the same way.

How the Encryption Works

For Cadence, the goal isn't just "hide the transactions." It's stronger, the goal is to let the validators agree on the order of proposals without any of them being able to read the transactions inside. Consensus runs blind. Only once the order is locked in does anything get decrypted. If you can't read a transaction until its position is already fixed, you can't front-run it.

To encrypt transactions, Cadence uses threshold encryption. Which means that one key is split into many, and each validator has a piece of the key.

Why not just use one decryption key? Because whoever holds it holds all the MEV power — they could decrypt early, peek at what's coming, and front-run everyone.

Threshold encryption. Instead, the decryption key is split into N shares, one per validator, during a one-time setup ceremony (a Distributed Key Generation, or DKG). No one ever holds the whole key. Any f + 1 = 4 shares reconstruct it; fewer cannot. f is the maximum number of faulty nodes the protocol can tolerate (33%).

The clever part: the shares ride on votes. A validator doesn't release its share whenever it likes. It attaches its share to its vote. So shares only become available as validators vote — that is, only when the block order is decided are transactions revealed.

So the two jobs from the top of the lesson — agree and reveal — happen through the same messages. A vote is simultaneously "I include this proposal" and "here's my piece of the key."

In your simulator, this is all assumed/invisible to you. There's no real DKG and no real ciphertext. A proposal just carries a encrypted flag, each validator's "key share" is just its own id, and "decryption" is flipping encrypted to decrypted once 4 distinct key pieces have shown up on votes. The logic — reveal only after f + 1 validators participate — is exactly the real thing; only the crypto is faked.

For the sake of this lesson, do not worry about how encryption happens, or the fine details of consensus. Just know:

  1. There is an encrypted mempool that has your private transactions
  2. Validators (proposers) pull from the mempool. Transactions are still private
  3. Transactions are only revealed after block order is determined.

Why split the decryption key into shares instead of giving one validator the whole key?

When does a validator release its piece of the decryption key?

How One Slot of Cadence works

Here's are all the steps:

  1. There are some number of validators. Of those validators, 3 are proposers. The proposers rotate as slots are filled.
  2. Propose: Each of the proposers builds a proposal, i.e. an encrypted bundle of transactions taken from an encrypted mempool. The proposer hashes the transaction and breaks it into chunks, and then sends every validator one chunk. Each validator can confirm the chunk is not tampered with by looking at the hash.
  3. First Vote. When the deadline passes, every validator casts one vote per proposer, based on its inbox:

Two things ride along with each vote:

  • The vote is an attestation of availability ("I saw it in time") and integrity (the chunk verifies against the id). It is not a check of the transaction contents — those are still encrypted.
  • Each vote carries the voter's key piece. Remember, each validator has a piece of the decryption key (threshhold encryption). Once the validator has f + 1 = 4 distinct pieces, it can reconstruct the decryption key and decrypt the proposal. This decryption happens once the block order is decided (a number of votes has already happened)

3. Commit Vote. Once a validator has decided every proposer (speculative finality), it broadcasts a commit vote on its recorded result. When 2f + 1 matching commit votes land (7 in our example), the slot is finalized.

4. The block. On finality, merge the transactions of all included proposals (drop duplicates, skip the "out" ones) into one block and append it to the chain. Then rotate proposers and move to the next slot.

Important note: Conceptually, Cadence is split into Chorus and Conductor. Steps 2-4 are called Chorus.

The scheduling of the slots themselves is handled by a second component called Conductor: it opens a new slot at a fixed interval whether or not the previous slot has finished, so several slots run at once. That trick is called extreme pipelining. Don't worry about memorizing this. The goal of this lesson is just to build a basic understanding!

What does a first vote actually attest?

What makes a slot final in Cadence?

Now let's build it.

Hands-On: Build Cadence

You'll prompt your agent one step at a time. Each step ends with a check. If it drifts or over-builds, stop it and re-paste the prompt, narrower.

For steps 3-5, make sure to read the prompt before you paste it! Try to not move on to the next step until you understand the previous step.

Step 0: Set Up Your Project

The prompts in Steps 1-7 are the same no matter which agent you use. The only difference is where you paste them.

Create a new App in Replit and open the AI panel. That's it. Replit handles the environment, the dev server, and the preview pane for you.

For each step below, paste the prompt into the AI panel. When the agent finishes, check the preview pane on the right against the step's Verify section. If the preview doesn't refresh on its own, click the refresh icon at the top of the pane.

Step 1: Scaffold the Dashboard (with the Validators slider + hover explanations)

I'm building a browser simulator of Cadence, a BFT blockchain consensus
protocol. No backend, no real cryptography — all state in memory.

- Scaffold a Next.js app (App Router, TypeScript, Tailwind), one route at /.
- Dark dashboard, max-width ~1150px, single column, sans-serif.
- Put the data model in a lib file. Define: a Validator { id, inbox: [],
  status: "Idle" }, and a SimState holding { slot: 1, deadlineMs: 150,
  clock: 0, validators, proposers, log: [], chain: [] }.
- Proposers for a slot rotate by ONE: slot 1 -> [0,1,2], slot 2 -> [1,2,3],
  wrapping mod N. Write proposersForSlot(slot, n). Slot 1 -> [0,1,2].
- Derive from N (compute, never hardcode):
    f = floor((N-1)/3);  Quorum = N - f;  Rebuild = f + 1
  For N=10: Quorum 7, Rebuild 4.

- TOP ROW: the page title on the left, and on the right a "Validators" range
  slider (min 4, max 22, step 1, default 10) with the current value shown.
  Changing it re-initializes the sim with that many validators; Quorum and
  Rebuild recompute from the new N.

- HEADER: "Slot X", "Deadline: 150 ms", "Clock: 0 ms", "Quorum: 7",
  "Rebuild: 4"; three stage buttons Propose / First Vote / Commit Vote (all
  DISABLED for now); a Reset button.
  Make "Quorum" and "Rebuild" HOVER TARGETS (dotted underline, help cursor).
  On hover, show a small tooltip with the live N and f substituted in:
    - Quorum: "Quorum = N - f = 7. With N=10 validators, up to f=(N-1)/3=3 can
      be Byzantine. A quorum of matching votes finalizes a decision; any two
      quorums overlap in an honest validator. Add validators -> f grows ->
      quorum grows."
    - Rebuild: "Rebuild = f + 1 = 4. A proposal splits into N chunks; any 4
      reconstruct it, so at least one honest chunk is always included even if
      f=3 withhold. Bigger set -> bigger f -> more chunks needed."

- VALIDATOR CARDS: a horizontal row, one per validator: "Validator N", a
  "Proposer" badge if it's a proposer this slot, a status pill, "inbox 0".
- NETWORK LOG: a scrollable panel, "No messages yet."
- CHAIN: a horizontal strip, "No finalized blocks yet."
- Single useReducer for SimState (actions come next). No consensus logic yet.

Verify: header shows Quorum 7 / Rebuild 4; 10 cards with 0,1,2 badged; the
Validators slider shows 10 and, when dragged to e.g. 7, the card count and
Quorum/Rebuild update (7 -> Quorum 5, Rebuild 3); hovering Quorum/Rebuild
shows the explanation; three disabled buttons; empty log and chain.

Step 2: The Simulated Network and Clock

Add a simulated network and clock. Everything from here flows through it.

- Message model: { from, to, type, payload, sentAt, arrivesAt, label? }.
  Delivery latency is 60ms +/- 30ms of SIMULATED time (random per message),
  so [30,90]ms. Add a helper deliveryDelay().
- Keep an inFlight array of scheduled messages and a simulated clock (ms).
- A ticker (setInterval via useEffect) advances the clock while there is
  anything to do, and dispatches a TICK. On each TICK: advance the clock a
  small step, deliver every message whose arrivesAt <= clock (push into the
  recipient's inbox), log each delivery. Freeze the clock when nothing is in
  flight and no deadline is pending.
- Keep the reducer PURE for now: roll randomness (delays) in the action
  creators and pass precomputed delays into actions. (We revisit this in
  Step 6.)
- Log format: timestamp + text. Plain deliveries: "Validator X -> Validator Y:
  <type>". Show a small green "active" pulse in the header while the clock is
  advancing.
- Add send(from, to, type, payload) on the hook that schedules one message
  through the delivery model. Nothing calls it yet.

Verify: nothing visibly changes yet, but the clock infrastructure is in place
and the app still renders and type-checks.

Step 3: Propose (with the proposal chip + hover)

Wire up "Propose" (enabled when no proposal exists yet this slot). When
clicked, each of the 3 proposers, simultaneously:

1. Creates a proposal: 3 fake transaction ids, marked locked: true (a stand-in
   for threshold encryption). Compute a short 4-hex-char id by hashing the
   proposer id + tx ids (a fake Merkle root).
2. Splits it into N chunks, one per validator: { proposerId, chunkIndex, id }.
3. Sends each validator its chunk through the delivery model.

Also:
- Set the slot deadline to 150ms after the click; show it counting down
  (deadline - clock). Keep the clock advancing to the deadline even after all
  chunks arrive, so the deadline actually passes.
- Proposer cards flip status to "Proposed".
- Under each proposer's card, show a small PROPOSAL CHIP: an "encrypted" badge
  on one line, and below it the word "proposal" followed by "id <hex>". Make
  the word "proposal" a hover target (dotted underline) with a tooltip:
  "The transactions are hashed into this id, then split into chunks — each
  chunk can be verified against the hash." Use a FIXED-position tooltip so the
  scrolling card row doesn't clip it.
- Log each chunk delivery: "Proposer 0 -> Validator 7: chunk (id a3f2)".
- Enable "First Vote" once the deadline has passed.

Don't have validators react yet — chunks just land in inboxes (each validator
ends with 3, one per proposer).

Verify: click Propose; 30 chunk deliveries log, proposer cards show
"encrypted" chips (hovering "proposal" shows the tooltip), the deadline counts
down, First Vote enables at 0, each inbox reaches 3.

Step 4: First Vote (voting, key pieces, decryption)

Wire up "First Vote" (enabled once the deadline passed). When clicked, every
validator casts one vote PER PROPOSER, based on its inbox:
- If proposer P's chunk arrived before the deadline: { type:"yes", id, chunk }.
- Otherwise: { type:"no" }.
Every vote carries keyPiece = the voter's id (its share of the decryption key).
Broadcast every vote to ALL validators through the delivery model. Flip all
cards to "Voted".

As votes land, tally per proposer (dedupe by voter+proposer):
- Under each proposal chip, show a live tally "yes 8 / no 0".
- Green check once quorum (7) yes match; gray "OUT" once it can't reach quorum
  (no >= N - Quorum + 1).

Decryption: each vote carries one key piece. Once a slot has collected Rebuild
(f+1 = 4) DISTINCT key pieces, the decryption key reconstructs and the
proposals decrypt: flip the chip "encrypted" -> "decrypted" and reveal the tx
ids. Log "4 key pieces collected — proposals decrypted".

Note: decryption (4 pieces) happens BEFORE quorum (7 votes) — different
thresholds.

Verify: after Propose + deadline, click First Vote. Tallies climb to yes 10 /
no 0 per proposer (chunks all arrived before the 150ms deadline since delivery
is <=90ms), each hits the green quorum check, and the chips flip to decrypted
with tx ids shown once 4 key pieces land.

Step 5: Commit Vote and Finalization

Wire up "Commit Vote". Introduce speculative finality first:

- A slot reaches SPECULATIVE FINALITY once every proposer is decided (quorum
  yes = included, or can't-reach-quorum = out). Record the clock time. Enable
  "Commit Vote" then.
- When Commit Vote is clicked: every validator broadcasts a commit vote
  carrying its recorded result (a digest of the included-proposal set) to all
  validators. Flip cards to "Committed".
- Tally commit votes. On quorum (7) MATCHING commit votes, FINALIZE the slot:
  record the clock time.

On finalization, build the block:
- Merge the transactions of all included proposals (quorum-yes ones), dropping
  duplicate tx ids, skipping OUT proposals.
- Append to the Chain strip. The block card shows: slot number, number of
  transactions, and two times measured FROM THE DEADLINE — "spec: Xms" and
  "final: Yms".
- Log "Slot 1 finalized. Included proposals merge into one block."
- Then advance: slot + 1, rotate proposers (slot 2 -> [1,2,3]), reset
  validators to Idle with empty inboxes, clear the per-slot state, re-enable
  Propose.

Verify: Propose -> (deadline) -> First Vote -> (speculative) -> Commit Vote.
A block appears with a tx count and both times. Run the cycle again and confirm
slot 2's block appears with proposers [1,2,3].

That's the fast path complete: one slot, driven by hand.

Step 6: Go Concurrent — Per-Slot State, the Conductor, and Auto-Run

The big refactor. Single-slot state becomes per-slot state, and a scheduler runs many slots at once. Expect to iterate. Get Steps 1–5 solid first, and keep manual mode working after this.

Refactor to run many slots concurrently, then add Auto-Run.

DATA MODEL:
- Introduce SlotState { slot, proposers, deadlineAt, proposals, votes,
  commitVotes, speculativeAt, finalAt, proposed, firstVoted, committed }.
  SimState now holds: clock, tau (default 150), autoRun, outage, validators,
  slots: SlotState[], chain, pending (finalized out of order), nextChainSlot,
  inFlight, log, and conductor fields (nextSlotNumber, nextProposeAt).
- Validator becomes just { id, inbox } — DERIVE its display status from the
  focus slot in the UI, not stored on the validator.
- Messages carry a `slot` field; route each delivery to its SlotState.

FOCUS SLOT (what the cards mirror): pick the MOST-ADVANCED in-flight slot
(committing > voting > proposing, tie-break by highest slot number) so the
encrypted -> decrypted -> voted progression is actually visible. Show a
"showing slot X" chip near the cards.

ARCHITECTURE:
- Move randomness (jitter, tx ids) INTO the tick now (the reducer becomes
  impure), and disable React StrictMode (reactStrictMode: false in
  next.config) so its double-invoke doesn't double-roll the randomness.
- Base each slot's tally on ONE representative validator's inbox (an
  "observer", e.g. validator 0) — the votes addressed to it — so quorum forms
  after a real network round, not an artificially-fast global first-arrival.
- SPECULATIVE FINALITY = all first votes for the slot delivered to the
  observer (this makes finalization take longer than one tau).

HEADER ADDITIONS:
- A "tau" slider (50-500ms, default 150). It sets the deadline window: in
  manual mode a slot's deadline is now clock + tau; in Auto-Run it's the block
  interval. (Rename deadlineMs -> tau.)
- An "Auto-Run" toggle.
- An "in flight" counter = slots past their deadline, not yet finalized.

AUTO-RUN: when on, a Conductor schedules a new slot every tau WITHOUT waiting
for earlier slots. For each slot it auto-fires: propose (chunks sent tau before
the deadline), first votes at the deadline, and commit votes once speculatively
final. Disable the three stage buttons while Auto-Run is on. Keep MANUAL mode
(buttons) working when Auto-Run is off.

LOG: tag every line with its slot number (e.g. "S6"), and keep only a tail
(cap ~200 entries) since volume is high.

Verify: manually run a slot as before (still works). Then toggle Auto-Run:
blocks appear roughly every tau, the "in flight" counter sits low and steady,
the cards cycle through slots showing decryption, and every log line is
slot-tagged. Move the tau slider and watch the cadence change.

Step 7: Ordered Chain, the Brake, and Outage

Finish the pipeline with four behaviors.

ORDERED CHAIN: append blocks to the chain in SLOT ORDER even when finalization is out of order. Keep a contiguous `chain` plus a `pending` set. When a slot finalizes: if it's the next expected slot, push it and drain any pending that now extend the chain; otherwise hold it in pending. Render pending blocks DIMMED with "waiting for slot X" (the earliest missing slot) until the gap fills.

BRAKE: think of slots grouped into windows of 8, the next window opening only when 6 of the current 8 have finalized — which bounds unfinalized slots at 8 + (8 - 6) = 10. Implement it as: the Conductor won't schedule a new slot while there are already 10 unfinalized slots in flight. When it's blocked it STALLS; on resume it starts scheduling "from now" (not catching up), which leaves a visible GAP in the deadline schedule.

FAULTY PROPOSER: a "Faulty Proposer" button. Clicking it makes the proposer of the NEXT scheduled slot go silent: its proposal is never sent, so that slot cannot finalize normally. After the slot's deadline passes plus a timeout (say 2x tau), validators give up and finalize it as an empty SKIPPED block. Meanwhile the clock keeps running and later slots keep finalizing, so their blocks sit dimmed in pending with "waiting for slot X" until the skipped block lands and the chain drains.

OUTAGE: an "Outage" toggle. While on, NO messages are delivered — they queue in inFlight. But the clock KEEPS ADVANCING (wall-time passes even while messages sit queued), so slots still pass their deadlines and the Conductor still schedules up to the brake cap. When the toggle turns off, the queued messages deliver.

Verify:
- tau=150 -> a block every ~150ms while finalization takes longer than 150ms; in flight hovers ~1-2.
- tau=50 -> a block every ~50ms (faster than one 60ms delivery); in flight higher but steady.
- Clicking Faulty Proposer -> that one slot misses its deadline, the next few blocks show dimmed with "waiting for slot X", in flight bumps up slightly but stays well under 10, then the skipped block lands, the pending blocks drain in a burst, and the beat resumes. The brake never engages.
- During an outage, in flight climbs and STOPS at 10; on recovery there's a burst of blocks, one visible gap in the schedule, then the steady beat resumes.

That's the whole thing — the exact app.

What You Just Built

Take a step back and look at what's running.

You implemented Cadence's fast path — the actual protocol shape. One slot moves through Propose → First Vote → Commit Vote, and out the other end comes a finalized block.

  • The proposers put forward encrypted proposals, fingerprinted with a Merkle-root id and erasure-coded into chunks — so no single sender is a bottleneck and any f+1 chunks rebuild the proposal.
  • The first vote does double duty: it's a BFT vote on availability + integrity, and it releases each validator's key share, so the proposal decrypts exactly as the network commits to it — never before. That's the encrypted-mempool, anti-MEV guarantee, expressed in one message.
  • Speculative finality (all proposers decided) gates the commit round, and 7 matching commit votes make it irreversible — because any two quorums of 7 share an honest validator.
  • The block merges the included proposals, and the chain grows in slot order.

Every hard idea appeared once, in context: 2f+1 quorum for safety, f+1 for both rebuild and decryption thresholds, Merkle roots for integrity, threshold encryption for order-fairness.

What's Different from Production Cadence

Your simulator captures the protocol logic; a production deployment layers real engineering on top:

  • Real cryptography. Real BLS signatures on votes, a real Distributed Key Generation (DKG) producing real threshold-encryption key shares, and real erasure coding with Merkle proofs per chunk. Yours uses a locked flag, keyPiece = validator id, and a fake 4-char hash.
  • A real network. Yours is a delivery model with random latency; production is a real p2p gossip network with real partitions and real adversaries.
  • The full pipeline. Production runs many slots concurrently with a real scheduler and back-pressure (your optional Auto-Run stretch is a taste of this).
  • Real transactions. Production encrypts a symmetric key inside the threshold ciphertext and uses it to lock full transaction bytes; yours encrypts tiny fake tx ids directly.
  • Malicious-validator handling. Production detects and slashes equivocation and invalid shares; yours assumes everyone honest.

The cryptographic and protocol core — proposers, quorum-based finality, encrypted-then-decrypt-on-vote, ordered chain assembly — is what you built.

What's the biggest difference between your simulator and production Cadence?

X (Twitter)

For a limited time, if you post a screenshot of what you built on X (Twitter), we'll retweet it from the @buildanythingso handle!

Glossary

  • Validator — a consensus participant; N of them.
  • Proposer — a validator proposing this slot; 3 per slot, rotating by one each slot.
  • Slot — one round of the protocol; produces at most one block.
  • f — max tolerated Byzantine validators; f = ⌊(N−1)/3⌋.
  • QuorumN − f = 2f + 1; matching votes needed to finalize. Two quorums always overlap in an honest validator.
  • Rebuildf + 1; chunks needed to reconstruct a proposal, and key shares needed to decrypt.
  • Chunk — one erasure-coded slice of a proposal, one per validator, carrying the proposal id.
  • Merkle root / id — a proposal's fingerprint; lets a validator verify a chunk belongs to it, unaltered.
  • Threshold encryption — the proposal is encrypted; the decryption key is split into N shares, any f+1 of which reconstruct it.
  • Key piece — one validator's share of the decryption key, released by attaching it to a vote.
  • First Vote — one yes/no vote per proposer, attesting availability + integrity; also carries a key piece.
  • Speculative finality — every proposer decided; a validator is confident but not yet committed.
  • Commit vote — a validator's attestation to its recorded result; 7 matching = finality.
  • Finality — irreversible agreement; the block is produced.
  • tau — the block interval / propose-to-deadline window.

You now have the most direct possible understanding of Cadence: not from reading about it, but from building a working model. Every consensus protocol is a different arrangement of the same moving parts you just wired together — proposers, votes, quorums, finality. With this in hand, the next protocol you read is approachable rather than opaque.

0/7 correct

0% — get all correct to complete

Sign Up to Track Progress