G.Poseidon2 Under Fiat–Shamir

This one needs a wider screen.

The visualizer keeps the transcript, the duplex sponge, both round machines and the full 254-bit state on screen at once and steps them together. Every part of it is meant to be read against the others — which is the whole point, and exactly what shrinking it onto a phone would take away.

It needs at least 1024 × 560 logical pixels — a landscape tablet, a laptop, or a desktop.

this screen: —

On a tablet, turning it to landscape is usually enough. Otherwise open this page on a laptop or a desktop.

Read what it does instead →
G. Poseidon2 Under Fiat–Shamir BN254 duplex transcript visualizer
Speed 160 ms
ready
step 0 / 0

Transcript

click to jump

Challenges drawn

0 of 6

Duplex sponge

idle rate 2 · capacity 1 · overwrite absorption
input_buffer
0
1
overwrite
rate r = 2 — the public half: absorbed into, squeezed from
s00x0
s10x0
squeeze
output_buffer
0
1
+ length
capacity c = 1 — sealed: no absorbed value lands here, nothing is read out
s20x0
no path out
the only thing that stirs the two halves together
Poseidon2 permutation
t=3 · α=5 · RF=8 · RP=56
ready
Press Step. The transcript starts with the sponge state all zero and both buffers empty.

Permutation

64 rounds · 8 external, 56 internal
0–3 external 4–59 internal 60–63 external
External round full rounds 0–3 and 60–63 3 constants · 3 S-boxes · 9 mults idle
Internal round partial rounds 4–59 1 constant · 1 S-box · 3 mults idle

Sponge state

persistent

Buffers

what is pending, what is available

What it cost

so far

Poseidon2 Under Fiat–Shamir

A production Fiat–Shamir transcript for a zkVM whose native field is the BN254 scalar field: the width-3 Poseidon2 permutation driving a persistent rate-2 / capacity-1 duplex challenger. Everything on this page is computed by the page, in BigInt arithmetic modulo the BN254 scalar modulus, from the reference round constants reproduced below.

The suite identifier is Poseidon2-BN254-Fr-t3-r2-c1-a5-RF8-RP56. Nothing here is about proving, arithmetization or commitment schemes — only about the transcript: what goes in, what comes out, and in what order.

Parameters

FieldBN254 scalar field Frnot the curve base field
Modulus p21888242871839275222246405745257275088548364400416034343698204186575808495617
State width t3
Rate r / capacity c2 / 1 — lanes s0, s1 are the rate; s2 is the capacity
S-boxx → x5 (α = 5, three multiplications each)
Full rounds RF8 — four before the partial rounds, four after
Partial rounds RP56 — S-box on lane 0 only
Round constantsthe exact width-3 BN254 RC3 table from the Horizen Labs reference instance, listed in full below

The two linear layers

Poseidon2's cost saving is the partial round, and a partial round is only sound if the linear layer still mixes every lane. Both matrices below are cheap: each is one shared sum plus a doubling.

ME — external, full rounds
211 121 112
u = s0 + s1 + s2
→ (s0+u, s1+u, s2+u)
MI — internal, partial rounds
211 121 113
u = s0 + s1 + s2
→ (s0+u, s1+u, 2s2+u)

One entry differs. That single change is what makes MI safe to iterate fifty-six times against a single-lane S-box, where repeating ME would not be.

The permutation

// the initial external linear layer comes before any constant or S-box
S ← M_E · S

// rounds 0..3 — external (full)
for r in 0..4:
    S    ← S + RC[r]          // all three lanes
    S[i] ← S[i]^5             // all three lanes
    S    ← M_E · S

// rounds 4..59 — internal (partial)
for r in 4..60:
    S[0] ← S[0] + RC[r][0]    // lane 0 only
    S[0] ← S[0]^5             // lane 0 only
    S    ← M_I · S

// rounds 60..63 — external (full)
for r in 60..64:
    S    ← S + RC[r]
    S[i] ← S[i]^5
    S    ← M_E · S

The duplex transcript

The state is [s0, s1, s2], initialised to all zeros, and it is kept across the entire proof — a fresh state per challenge would break the binding that Fiat–Shamir depends on. Two buffers sit alongside it.

The three lanes are not peers. s0 and s1 are the rate: the public half, the only lanes absorbed material is written into and the only lanes challenges are read out of. s2 is the capacity, and it is sealed — no absorbed value is ever placed there and nothing is ever squeezed from it. It is where the sponge's security margin lives, and an adversary who can set it or read it has broken the construction. Exactly two things touch it: the absorbed-length tag, and the permutation. The diagram draws that literally — the rate sits on the horizontal path between the two buffers, the capacity sits off it.

observe(x):
    output_buffer.clear()          // any unconsumed challenge is now stale
    input_buffer.push(x)
    if input_buffer.len() == 2: duplex()

sample():
    if input_buffer non-empty or output_buffer empty: duplex()
    return output_buffer.pop()   // from the END

duplex():
    n = input_buffer.len()
    if n > 0:
        state[i] = input_buffer[i]   // OVERWRITE, not add
        state[n..2] = 0              // zero the unused rate lanes
        state[2] += n                // bind the absorbed length into the capacity
    state = Poseidon2(state)
    input_buffer.clear()
    output_buffer = [state[0], state[1]]

Four details in there are easy to get wrong, and each of them changes the challenges:

Provenance. All four rules are Plonky3's, not a variant of it: its DuplexChallenger clears the rate slots the inputs did not overwrite and adds the absorbed length into sponge_state[RATE], in its own words to make the absorb “prefix-free so length and zero-padding cannot collide.” This page reproduces its challenges exactly. Two version notes worth carrying into a review: length binding landed in Plonky3 #1769 (June 2026), so an implementation pinned to anything earlier will produce different challenges from the same inputs; and upstream stores the tag as a u8, so absorb lengths are distinguishable only up to 255 — irrelevant at rate 2, but not at wider rates.

A duplex called from sample() with nothing buffered does none of the absorb work — no overwrite, no zeroing, no length tag. It just permutes and refills the output buffer. The last call of the loaded transcript is that case.

Getting bytes into the field

An arbitrary byte string must not be read as a 256-bit integer and reduced mod p: that map is not injective, so two different strings can produce the same field element. The encoding used here absorbs the byte length first, then splits the string into 31-byte chunks read as unsigned little-endian integers. At most 248 bits, every chunk is already below p and needs no reduction.

The loaded transcript starts from field elements already, so this step is not shown on the diagram — but it is where the elements a real protocol absorbs come from, and it is one of the rules that has to be frozen alongside the transcript itself.

Known-answer test

The reference BN254 width-3 instance fixes one test vector. If an implementation does not reproduce it, nothing downstream of it is compatible. This runs the permutation on the page, right now.

input   [0, 1, 2]
not run

Round constants — RC3, all 64 rounds

Full rounds carry three constants; partial rounds carry one, and the reference table stores the two unused lanes as zero. Nothing here is generated at runtime.

rlane 0lane 1lane 2

Keys

Space step   back   Enter run / pause   K skip the permutation   R reset   123 stage / round / call   ? this sheet   Esc close

Sources