A production Fiat–Shamir transcript, stepped one stage at a time. Not one challenge at a time — one constant added, one S-box evaluated, one matrix applied: the width-3 Poseidon2 permutation over BN254 running inside a persistent duplex challenger, with every field element on screen at every point. It answers the question you actually have when two implementations disagree on a challenge: which of the sixty-four rounds, or which of the six duplex rules, did we do differently?
Scope. This is about the transcript and nothing else. There is no proving system here, no arithmetization, no commitment scheme — only what goes into the sponge, what comes out, and in what order. That is deliberate: the transcript is the part two independent implementations of the same protocol most often disagree about, and it is the part that is easiest to get subtly, silently wrong.
Everything on screen is computed by the page, in BigInt arithmetic modulo the BN254 scalar modulus, from the reference round constants. The spec sheet will run the mandatory known-answer test in front of you.
It wants 1024 × 560 logical pixels or more — a landscape tablet, a laptop, a desktop. The call being executed, the sponge, the round machine and the full state are meant to be read against each other, so on anything smaller the page offers this write-up instead of a version with two thirds of it hidden.
The profile is frozen to the established width-3 BN254 Poseidon2 instance driving a rate-2 / capacity-1 duplex challenger — the Plonky3 transcript framing on the Horizen Labs permutation:
| Parameter | Value |
|---|---|
| Field | BN254 scalar field Fr — not the curve base field, which is a different prime of the same size and the most common way to get this suite wrong |
| State width | t = 3, as rate 2 + capacity 1 |
| S-box | x → x5 — three field multiplications each |
| Rounds | RF = 8 full and RP = 56 partial, split four–fifty-six–four |
| Constants | the exact RC3 table from the reference instance, listed in full in the spec sheet — never regenerated at runtime |
Poseidon2's whole economy is the partial round, and the tool is built around making that visible. An external round and an internal round are different circuits, so they get two separate machines, both permanently on screen, each with its own wiring and its own matrix. Nothing is ever re-labelled in place. Each is a dataflow — three lane rows, three stage columns — and the one matching the current round lights up and fills with values while the other stays dark.
- In an external round — four at the start, four at the end — all three lanes take a round constant, all three go through the S-box, and the layer is
ME. Nine multiplications. - In an internal round — the fifty-six in the middle — only lane 0 takes a constant and only lane 0 goes through the S-box. Lanes 1 and 2 are drawn as pass-throughs, and the layer is
MI. Three multiplications.
Because each machine keeps its own matrix, the two sit one above the other and exactly one entry differs: the bottom-right is 2 in ME and 3 in MI. That one change is what makes it safe to iterate the internal layer fifty-six times against a single-lane S-box, where repeating the external one would not be. A sixty-four-cell timeline above the diagram shows the shape of the whole permutation at a glance — the cheap rounds are the overwhelming majority — and clicking any cell jumps to that round.
A running cost panel keeps the arithmetic honest: one permutation is 80 S-box evaluations and 240 field multiplications, where sixty-four full rounds would be 192 S-boxes for the same round count. The linear layers cost no multiplications at all.
The duplex is a handful of lines of pseudocode, and four of those lines change every challenge downstream if you write them differently. The loaded transcript is built to walk into each one:
| Rule | What breaks without it |
|---|---|
| Absorption overwrites | The rate lanes are replaced by the absorbed values, not summed into. A sponge that adds instead produces entirely different challenges from the same inputs — and every individual operation still looks correct. |
| Unused rate lanes are zeroed | On a one-element absorb, lane 1 is cleared rather than left holding the previous permutation's output, which would silently bind the challenge to unrelated history. |
| The length is bound into the capacity | state[2] += n is what makes absorbing one element distinguishable from absorbing two whose second is zero. Without it those two transcripts collide, and a prover gets to choose between them. |
| Challenges come off the end | After a permutation the output buffer holds [s0, s1] and pop() takes the last, so the first challenge is s1 and the second is s0. Pure convention — and reversing it makes every challenge wrong. |
These are Plonky3's rules, not a variant of them. Its DuplexChallenger clears the rate slots the inputs did not overwrite and adds the absorbed length into sponge_state[RATE] — in its own comment, to make the absorb “prefix-free so length and zero-padding cannot collide.” The visualizer reproduces its challenges exactly.
Two version notes for anyone diffing against a peer. Length binding landed in Plonky3 #1769 in June 2026; an implementation pinned to anything earlier overwrites the rate and permutes with no padding and no tag, and will produce different challenges from the same inputs. And upstream stores the tag as a u8, so absorb lengths collide modulo 256 — irrelevant at rate 2, but not at wider rates.
Two more behaviours get their own moment in the script: observing new material invalidates any challenge still sitting in the output buffer (the discarded slots are drawn in red as it happens), and a sample() with nothing buffered and nothing left to squeeze runs a permutation with no absorb at all — no overwrite, no zero-padding, no length tag.
Eight calls, four permutations, three challenges, on one continuous state — the shortest script that still walks into every rule above, and nothing else.
- Five elements go in, one at a time. The rate holds two, so the sponge permutes each time it fills — twice — and the fifth is still buffered when the section ends. This is the case people most often get wrong when they assume one
observemeans one permutation. - A challenge is asked for on a partial buffer. The pending element must be absorbed first, and a one-element absorb is the only path that exercises the zero-padded lane and the length tag together.
- A second challenge comes free off the same permutation, from the other rate lane, in the frozen order.
- A third is asked for with nothing left — buffer empty, both outputs consumed — so the duplex permutes again with no absorb at all.
Invalidation falls out of the first section rather than needing its own: calls three and five each arrive while two rate outputs are still buffered, and discard both.
- Duplex sponge — drawn so the two halves of the state are not peers. The rate sits on the horizontal path between the input and output buffers, because it is the only thing absorbed into and squeezed from. The capacity sits off that path, dashed and hatched, with the one arrow that enters it (the length tag) and a struck-through stub where a squeeze would be. It lights up for exactly two things: that tag, and the permutation — which is the only thing that stirs the two halves together.
- Two round machines — external and internal, stacked and always both visible, with the values after the constant, after the S-box and after the linear layer filling into whichever one is running. Lanes a machine never touches are drawn as pass-throughs in its own box, so the shape of the difference is on screen even when neither is lit.
- Phase rail — buffer, overwrite, zero pad, + length, permute, squeeze. Phases a given call skips are struck through, so the no-absorb duplex is legible as an absence.
- Narration — a sentence per stage saying what just happened and, where it matters, what would break if it happened differently.
- Full field elements — all three lanes at their full 254 bits, in hex or decimal, alongside both buffers and every challenge drawn so far with the lane it came from.
- Spec & constants — a drawer with the parameters, both matrices, the permutation pseudocode, the duplex pseudocode, the byte-encoding rule, all 64 rounds of
RC3, and a button that runs the mandatory known-answer test on[0, 1, 2]in the page and shows you the verdict. - Reversible — step backwards a stage at a time; the state, both buffers and every counter restore exactly, because the whole transcript is precomputed as snapshots rather than replayed.
