Read this page to understand the machine behind every proof on Rand: what it proves, how a run of a program becomes something a validator can check in milliseconds, and where the privacy comes from. For what a proof costs and how the wallet makes one, see Proofs. For the parameters and hash constructions, see Cryptography. For the instruction set a program is written against, see The RISC-V ISA.
The one statement every proof makes
The zkVM proves one kind of statement, for every program the chain runs:
A program whose digest is
hc, started atbase_pc, run on a private input committed to byH_INand a public input segment committed to byH_PUB, halted within2ᵗ − 1cycles, spent at mostGAS_LIMITgas and wrote these eight output words.
A shielded transfer is that statement about one fixed program, the hidden-asset bundle guest, whose hc is pinned in the genesis file as hc_bundle. Since chain 17 it is made twice per transfer: a second, small proof about the auth guest (hc_auth in the genesis file) shows that the sender holds the spend key (Proofs); since chain 21 that second proof may instead be the multisig auth guest’s (hc_auth_multisig), which shows that enough of a hidden set of Dilithium2 keys signed, without telling which set or how many. A confidential call is the same statement about a deployed program. There is one machine, one verifier and one set of constraints for all of them. Deploying a program is therefore a data operation (store its words and hc), not a cryptographic one: there is no per-program circuit and no trusted setup.
The proof system is a STARK built with Plonky3. It needs nothing but a hash function: no setup ceremony and no elliptic curves, so it has nothing for Shor’s algorithm to break.
The pipeline
secret input ──► run the program ──► execution trace ──► polynomials ──► commit ──► FRI ──► proof
(keys, (RV32IM emulator) (nine tables, (one per (8× code, (80 queries,
notes, paths) one row per step) column) Merkle) 20-bit grinding)
every validator: recompute the challenges from the Merkle roots, open 80 random positions,
check the rules there → ≈ 16 ms
The top row runs once, on the sender’s machine, in about 100 seconds for a bundle and about 7 seconds for the authorisation proof beside it. The bottom row runs on every validator, from the public fields and the proof alone.
1. Run the program and write down everything
The machine is a 32-bit RISC-V processor: RV32IM minus FENCE, the CSR instructions and EBREAK (The RISC-V ISA). The prover first runs the program on an emulator and records one row per clock tick. The record is the execution trace. Anyone holding it could replay the run step by step; the rest of the scheme exists so that a verifier can be convinced the trace is right without holding it.
A program talks to the outside through eight syscalls and nothing else:
| syscall | no. | what it does |
|---|---|---|
HALT | 0 | stop; the eight output words are final |
WRITE_OUTPUT | 1 | set one of the eight public output words |
READ_INPUT | 2 | read a word of the private input |
POSEIDON2 | 3 | hash a buffer in memory in place |
KECCAK | 4 | one Keccak-f[1600] permutation |
SHA256 | 5 | one SHA-256 compression |
READ_PUBLIC | 6 | read a word of the public input segment |
POSEIDON2_LEN | 7 | POSEIDON2 with the message length bound into the hash (constraint set 7) |
MODQ | 8 | one operation mod q = 8,380,417, Dilithium’s prime, on 256-coefficient polynomials in memory: an NTT, an inverse NTT, a four-way dot product or a multiply-subtract (constraint set 9, chain 21). It adds the optional modq table to the proof; only the multisig auth guest calls it, and a Dilithium2 verification costs 80,572 cycles with it against 880,102 in plain instructions |
Note commitments, nullifiers and Merkle paths are not built into the machine. They are ordinary guest code that stages a domain-tagged message in memory and calls POSEIDON2. That syscall does not pad, so [a] and [a, 0] hash alike. The shipped guests call it on fixed-length messages only. A new program that hashes data whose length can vary uses POSEIDON2_LEN (The RISC-V ISA).
There is no abort
The processor has no instruction that refuses to finish, so a program cannot stop when a check fails. The bundle guest records the failure instead: each failed check ORs a 1 into a register called bad, and bad is the last word hashed into the output digest:
D = H(64; anchor, nf₀..₃, cm₀..₃, fee, burn_a, burn_r, burn_asset, time, c, bad)
c is the authorisation commitment that the auth proof also publishes (since chain 17; the digest of chains 14 to 16 had no c).
The chain never takes D’s preimage from the sender. It rebuilds the preimage from the transaction’s public fields, sets bad = 0 itself, hashes, and compares. A cheater can make a perfectly valid proof of a run in which a check failed, but that run printed H(…, 1), and the chain is looking for H(…, 0). Matching them needs a hash collision.
2. Split the trace into nine tables
One table describing a whole processor would be wastefully wide, so the trace is split into specialist tables, proved together as one batch:
| table | rows at tier t | what it records |
|---|---|---|
program | declared, ≥ 2⁷ | the program’s instruction words, decoded in circuit; fixes what the CPU may fetch |
cpu | 2ᵗ | one row per cycle, preceded by rows that hash the program, the private input and the public segment; holds the 35 public values |
memory | declared, ≥ 2ᵗ⁺² | registers and RAM, sorted by (space, address, time) |
alu | 2ᵗ⁺¹ | every arithmetic, logical, shift, compare, multiply and divide, on byte limbs |
range, nibble | 256 each | fixed look-up tables: bytes, powers of two, 4-bit AND/OR/XOR |
poseidon2 | 2ᵗ⁺² | the hash permutation, one row per round |
input | declared, ≥ 2⁷ | the committed private input words |
public | declared, ≥ 2⁷ | the public segment words (for a bundle, the transaction binding) |
Two more tables, keccak and sha256, are present only in a proof whose program called them, each at least 2⁷ rows when present; the bundle guest uses neither. Constraint set 9 (chain 21) adds a third optional table the same way, modq: one 1,024-row block per MODQ call, declared in a new header field, modq_log_height, and carried only by the multisig auth guest’s proofs; the cpu table gained the syscall’s columns, which is why every verifier key moved. The floor of 2⁷ rows on every declared table came with constraint set 7 (section 9).
The tier t is one of 10, 12, 14, 16, 18 or 20 and sizes the tables. Every bundle is proved at tier 14: a 16,384-row CPU table, of which the guest uses about 12,400. The auth guest runs 250 cycles at tier 10; the multisig auth guest 85,830 to 251,946 cycles at tier 18 for one to three signatures, with every height that depends on the count floored at three, so a 1-of-N spend looks like a 3-of-N one. The rest is padding, so a proof reveals which tier a program needed, never how many steps it took inside it (Tiers).
The memory table is what makes RAM checkable. Because it is sorted by address and then time, “a read returns the last value written to that address” becomes a rule between two adjacent rows.
3. Connect the tables with buses
How does the verifier know the sum the CPU used is the sum the ALU computed? Through buses. When the CPU needs an addition it sends a message (op, a, b, c); the ALU receives one message for each row it computes. The machine has seventeen buses:
| bus | between |
|---|---|
PROGRAM, PROGRAM_WORD | cpu ← program: instruction fetches, and the words the program digest absorbs |
MEMORY | cpu ↔ memory: every read and write |
ALU | cpu → alu |
RANGE8, POW2, AND4, OR4, XOR4 | byte range checks, shifts and bitwise operations against the fixed tables |
POSEIDON2 | cpu → poseidon2: one message per permutation |
INPUT_DIGEST, INPUT_READ | cpu ← input: the words the input digest absorbs, and the program’s reads |
PUBLIC_DIGEST, PUBLIC_READ | cpu ← public: the same pair for the public segment |
KECCAK, SHA256, MODQ | cpu → the optional hash tables, and (constraint set 9) the mod-q table |
BLIND | every table → the next table in a fixed cycle: one random value each, on the first row (constraint set 7) |
The buses are checked with a logarithmic-derivative argument (LogUp). After the tables are committed a random value α is drawn, and each table keeps a running sum of ±1/(α − m) over the messages it sends or receives. The sums cancel exactly if and only if every message sent was received, the same multiset on both sides. One forged message leaves a residue that a random α exposes with overwhelming probability. The memory bus uses a close cousin, a multiset permutation check.
Each table publishes the final value of its running sum, its terminal, and the verifier checks that the terminals add up to zero. Through constraint set 6 a terminal was a checkable function of its table’s contents. Since constraint set 7 every table carries five extra columns and, on its first row, sends one random value and receives its neighbour’s on the BLIND bus. Each terminal moves by a uniform element of the challenge field, and the sum stays zero.
The digest and read buses are deliberately separate pairs. LogUp balances per message, not per consumer, so one shared bus would let a prover drop a word from the digest while a read of that word still succeeded.
4. Anchor the trace to the outside world
Thirty-five public values leave the trace, all in the CPU table:
| values | what they are |
|---|---|
| 1 | the entry pc |
| 1 | the tier |
| 8 | the output words (for a bundle, the digest D) |
| 8 | hc, the program digest, computed inside the trace from the program table’s words |
| 8 | H_IN, the private input digest, over the input with a fresh 128-bit salt first |
| 8 | H_PUB, the public segment digest, unsalted |
| 1 | GAS_LIMIT, the gas ceiling the prover declares (constraint set 8) |
Because hc is computed in circuit, a proof about a different program cannot pass as a proof about the bundle guest. The verifier never sees an instruction word; it compares hc with the digest it expects. Because H_IN is salted, it binds the private input without revealing it, even to someone who could guess it. Because H_PUB is unsalted, the verifier can recompute it from words it already has. For a bundle those words are the transaction binding, blake3("rand-tx-bind-1" ‖ chain_id ‖ tx) over the transaction with its proof bytes blanked, so a proof copied onto a transaction with a different destination, memo, envelope or chain id does not verify. Since chain 20 (genesis binding_domain: 1) it is blake3("rand-tx-bind-2" ‖ genesis_hash ‖ chain_id ‖ tx), so a proof does not verify on another chain even under the same chain id; the words are a public input, so this moved nothing in the circuit. The auth proof is made over the same eight words. A call proof since chain 20 is bound the same way: its segment is the program’s deploy-time public words followed by eight words of a call binding (rand-call-bind-2, rand-call-bind-1 before chain 20), so a call proof copied under another fee bundle does not verify.
Gas is the machine’s measure of a run’s weight, in the circuit since constraint set 8. The CPU table carries a GAS column that starts at 1 and adds 1 for every row, 3 for a Poseidon2 absorb row, 64 for a SHA256 row and 192 for a KECCAK row. On the HALT row the circuit proves GAS ≤ GAS_LIMIT through four range-checked limbs of the difference, which are never opened: the limit is public and the gas a run spent is not. The verifier refuses, before it builds a key, a limit above what the proof’s own header could need:
gas_max(t, k, s) = (2ᵗ − 1) + 2ᵗ⁻² + 191·(2ᵏ / 32) + 63·(2ˢ / 64)
with k and s the declared log heights of the keccak and sha256 tables, each term zero for an absent table. A hash-free tier-10 header’s ceiling is 1,279 and a tier-14 one’s 20,479. The chain prices a call by its declared limit (Fees).
5. Turn the rules into polynomial identities
Every column is a list of numbers in the Goldilocks field, integers modulo p = 2⁶⁴ − 2³² + 1. A list of n numbers is the same thing as a polynomial of degree below n: the one that passes through them when they are laid out on a subgroup {1, ω, ω², …} of the field. Goldilocks has such subgroups of every power-of-two size up to 2³², so the conversion is a fast Fourier transform.
A rule of the machine then becomes an equation between column polynomials. “The program counter advances by four unless the instruction jumps” becomes, schematically:
is_jump(x)·(pc(ωx) − target(x)) + (1 − is_jump(x))·(pc(ωx) − pc(x) − 4) = 0
for every x in the subgroup, where pc(ωx) means “the next row”. A table’s full set of such rules is its AIR (algebraic intermediate representation). The CPU and ALU rules reach degree 8; the others stay at 2 to 4.
A polynomial that is zero at every point of the subgroup is divisible by Z(x) = xⁿ − 1. The prover combines all the rules, with random weights, into one polynomial C(x) and divides by Z(x). If every rule holds on every row, the quotient is a genuine low-degree polynomial. If one rule fails on one row, it is not. “Was every step of a 16,000-cycle run correct?” has become “is this function close to a low-degree polynomial?”, and that question can be answered by looking at a few values chosen at random.
6. Commit before the questions are known
The prover must fix its polynomials before it sees the verifier’s random questions, or it could tailor its answers. It evaluates every column on a domain eight times larger than the table (blow-up 8, code rate 1/8) and publishes a Merkle root over the evaluations. That redundancy is a Reed–Solomon code: a low-degree polynomial is fixed by any eighth of its values, so a cheater who changes even one row of the trace has to change most of the extended codeword to stay consistent, and random spot checks will land on the damage.
The Merkle trees use Poseidon2 over Goldilocks: width 8, S-box x⁷, 4 full rounds, 22 partial rounds and 4 full rounds, digests of four field elements (about 256 bits). Every leaf is hashed with four elements of fresh randomness, so a root reveals nothing about the values beneath it. The same permutation is the note commitment and nullifier hash inside the machine. Its 86 round constants are a literal table in the node’s source (poseidon2_constants.rs), so no library upgrade can change them.
7. Spot-check with FRI
The verifier now holds Merkle roots. It first ties the quotient to the trace: it picks a random point z outside the table’s domain, asks for every column’s value and the quotient’s value there, and checks C(z) = Q(z)·Z(z) directly. What remains is to confirm that the committed functions really are low-degree polynomials, and that is what FRI tests.
FRI works by folding. Split a polynomial into even and odd parts, f(x) = fₑ(x²) + x·fₒ(x²). For a random β from the verifier, f′(y) = fₑ(y) + β·fₒ(y) has half the degree on a domain half the size. The prover commits to f′, gets a new β, and folds again (Rand folds by up to eight per round) until a constant is left, which it sends in the clear. A query then picks one random position in the first layer and follows it down, checking at every layer that the values opened above fold into the one below. A function that was far from low-degree cannot keep all those promises consistent.
Rand runs 80 queries. Under the proven proximity-gaps theorem, each query leaves a cheater at most a 9/16 chance, so 80 queries give (9/16)⁸⁰ ≈ 2⁻⁶⁶·⁴. Under the ethSTARK conjecture, which deployed STARKs rely on, each query leaves 1/8, which is far smaller.
8. Remove the conversation
Everything above was described as a dialogue, but a transaction has to be one message that any validator can check alone. The Fiat–Shamir transformation removes the verifier: the prover computes each “random” challenge as a hash of everything committed so far, with a Poseidon2 duplex sponge. Changing any commitment changes every challenge after it, so the prover cannot pick commitments to suit the challenges.
Two further measures close the gaps this leaves:
- Extension field. A field of
2⁶⁴elements is too small to draw challenges from: a cheater needs a challenge to land on one of a polynomial’s few bad values, and succeeds with probability aboutN/2⁶⁴however many queries are run. So the trace stays in Goldilocks, but every challenge is drawn from its quadratic extension, about2¹²⁸elements. - Grinding. Before the query positions are derived, the prover must find a nonce whose hash with the transcript starts with 20 zero bits, about a million hashes. An honest prover pays that once. A cheater re-rolling the query positions until they miss its forgery pays it on every attempt. This is the only proof of work anywhere in Rand, and it has nothing to do with consensus.
Adding up: 66.4 bits from the queries plus 20 from grinding gives 86 bits proven, and about 100 bits under the conjecture, where the extension field’s term is the limit.
9. Where the zero knowledge comes from
A STARK is not zero-knowledge by itself: the values opened at the query positions are values of the trace polynomials, and without care they leak. Rand’s configuration adds four layers:
- every committed polynomial is randomised (Plonky3’s hiding FRI mixes in four random codewords), so the 80 opened values can be reproduced, in distribution, by someone who never saw the trace;
- every Merkle leaf is salted, so roots and authentication paths reveal nothing;
- the prover’s randomness is drawn fresh from the operating system for each proof, so two proofs of the same payment look unrelated;
- every declared table has at least
2⁷rows and every LogUp terminal is blinded (constraint set 7). A proof opens each table at 80 query points and two out-of-domain points, and the hiding commitment mixes in as many random rows as the table has rows, so a table is hidden only while it has at least 82 rows. 128 is the smallest power of two that does.
Layer 4 is a correction. The 2026-09-27 review recovered the private inputs of calls with 31 or fewer input words from production proofs, read a program’s fetch counts from its short program table, and told a bundle’s real input slots from its dummies by its unblinded terminals. Those proofs are the ones made on chain 15 and earlier. Chain 16 (constraint set 7, with the branch-free bundle guest) closed all three, and chains 18 to 20 keep the fixes.
The guarantee is statistical zero knowledge in the random-oracle model. What a proof does not hide is exact: its tier, its declared table heights, and its public values, the gas limit among them. A declared keccak, sha256 or modq height of 0 means the run never used that table, so a call proof’s header shows whether the call hashed and, above the floor, how often to within a factor of two. For a bundle the public values and fields are the anchor, four nullifiers, four commitments, fee, burns, burn asset, time, the authorisation commitment and the transaction binding. The asset a transfer moves is a private input, so a zUSD payment and a RAND payment look the same on chain. hc is deterministic, so anyone can tell a proof concerns the bundle guest (every value-moving proof does) or, for a call, which deployed program it ran.
10. What a validator checks
Machine::verify_public(hc, words, proof) runs, cheapest first:
- the proof’s declared tier and table heights are bounded (
check_declared_heights) before anything is sized from them, so a malformed header costs a few comparisons, not a multi-second key build; a bundle must declare tier 14 with neither hash table nor amodqtable, a single-key auth proof tier 10, and a multisig auth proof exactly its pinned tier-18 shape (akeccaktable of2¹⁴rows, amodqtable of2¹⁶, a memory table of2²⁰); every call, invoke and state proof refuses amodqtable before any key is built; - there are 35 public values, each a canonical field element, with
pv[HC] = hc,pv[TIER]equal to the proof’s tier, every output word below2³², andGAS_LIMITat mostgas_maxof the header; - the batch STARK verifies against a verifier key cached per shape
(tier, program, input, keccak, sha256, public heights), not per program; H_PUBequals the digest ofwords, which for a bundle is the transaction binding the node computed itself.
Before any of this the node screens every submitted or gossiped proof for a canonical encoding: its FRI folding schedule must be the one an honest prover makes for the degrees the header declares. Since fullnode v0.6.8 (audit v6, ZKV-4) the screen first refuses any declared degree above 25 bits (MAX_HONEST_DEGREE_BITS, the memory table’s 2²⁴ ceiling plus the zero-knowledge doubling, the most an accepted proof carries). Before that bound, a header declaring a huge degree made the node re-derive an enormous folding schedule on its event loop, holding it for as long as the sender liked; the verifier would have refused the proof anyway, so the bound changes only the cost of saying no. A panic inside the screen now refuses that one transaction rather than stopping the node.
The ledger then recomputes the bundle digest D with bad = 0 and compares it with the proof’s eight output words. Since chain 20 it also requires a bundle proof’s GAS_LIMIT to be exactly the genesis bundle_gas_limit, 20,479, and a single-key auth proof’s to be 1,279 (on chain 21 a multisig auth proof’s 436,159), so every transfer declares the same values. With a warm key the whole check takes about 16 ms.
The parameters, and why they were chosen
Every number below is consensus: the verifier checks it, a proof made with any other value does not verify, and changing one is a new constraint set and a hard fork (Constraint sets). The FRI profile is fixed by the genesis file (fri_profile: production on chain 21, as on chain 20), never by the proof.
| parameter | value | why this value |
|---|---|---|
| base field | Goldilocks, p = 2⁶⁴ − 2³² + 1 | A 64-bit prime whose arithmetic is a few machine instructions, with multiplicative subgroups of every power-of-two size up to 2³², so turning a column into a polynomial is a fast Fourier transform. The trace lives here because it is the cheap field. |
| challenge field | quadratic extension F_p² (≈ 2¹²⁸) | Challenges drawn from Goldilocks itself would leave a soundness floor of about N/2⁶⁴ that no number of queries can lower. The extension pushes that term to at most 2⁻¹⁰⁰ for traces up to 2²⁵ rows. Only the challenges pay for the bigger field; the trace does not. A cubic extension (≈ 2¹⁹²) is the named margin if 128 bits is ever required. |
| blow-up | 8 (rate ρ = 1/8, log_blowup = 3) | Two reasons at once. The CPU and ALU tables have rules of degree 8, and with the zero-knowledge flag the quotient has degree 9, which is the most a blow-up of 8 can carry; a higher-degree rule would force blow-up 16. And each query’s power to catch a cheater depends on the rate: (1 + ρ)/2 = 9/16 per query proven, ρ = 1/8 conjectured. A larger blow-up would buy fewer queries at the cost of a proportionally larger low-degree extension for the prover to compute and hold in memory. |
| FRI queries | 80 | The number that keeps the proven soundness at about 86 bits. It was cut to 27 once (constraint sets 3 and 4), which met a conjectured 100-bit target (3·27 + 20 = 101) but left only about 42 proven bits; the 2026-09-12 zk audit (finding ZM1) restored 80. Queries are most of a proof’s size, which is why a bundle proof is ≈ 1.50 MB. A provable 100 bits would take 97 queries. |
| grinding | 20 bits, in the query phase | Before the query positions are drawn the prover must find a nonce whose hash starts with 20 zero bits: about a million hashes, paid once by an honest prover and on every retry by a cheater re-rolling positions. It adds 20 bits cheaply; more grinding (34 bits would also give a provable 100) makes every honest proof slower. The commit phase has no grinding. |
| folding arity | up to 8 per round (max_log_arity = 3) | Folding by eight instead of two gives a third as many FRI layers, so each query opens fewer Merkle paths and the proof is smaller, for a little more work per layer. |
| final polynomial | a constant (log_final_poly_len = 0) | FRI folds all the way down and sends one value in the clear, the simplest end check for the verifier. |
| hash (in circuit, Merkle, transcript) | Poseidon2, width 8, S-box x⁷, 4 + 22 + 4 rounds, rate 4, capacity 4 | A hash built to be cheap as field arithmetic, so hashing inside a proof (commitments, nullifiers, Merkle paths, the program digest) costs rows, not thousands of bit operations. The S-box exponent must share no factor with p − 1 = 2³²·3·5·17·257·65537, and 7 is the smallest that works. Width 8 at rate 4 gives a four-element digest, about 256 bits. The 86 round constants are a literal table in the node (poseidon2_constants.rs). |
| digest width | 4 field elements (≈ 256 bits) | Enough for the preimage roles (nullifiers, commitments: 128 bits against Grover). It is not enough for 128-bit quantum collision resistance in the transcript (2²⁵⁶/³ ≈ 2⁸⁵); the whitepaper’s design asks for a 384-bit transcript hash, and the running chain has not adopted it. |
| hiding | 4 random codewords in FRI; 4 salt elements per Merkle leaf; fresh OS randomness per proof | The minimum that makes the 80 opened positions and the Merkle paths simulatable from public values alone, so the proof is statistically zero-knowledge. Proofs of the same payment made twice look unrelated. |
| table floor | 2⁷ rows for every declared table (constraint set 7) | A proof opens 80 query points and 2 out-of-domain points of each table, 82 in all, and a table of h rows is blinded by h random rows. 64 rows are too few and 128 is the next power of two. A query count above 126 would need 2⁸. |
| terminal blind | one value of F_p² per table, on the BLIND bus (constraint set 7) | A LogUp terminal is computed from the raw trace, so without a blind it is an unsalted commitment to its table. The blind is an extension-field element because one base-field coordinate would confine the shift to a thin set that a candidate witness could be tested against. |
| gas weights | 1 a row; 3 a Poseidon2 absorb row; 64 a SHA256 row; 192 a KECCAK row (constraint set 8) | Each weight counts the rows the row pulls into other tables: an absorb row one 32-row permutation, a SHA256 row 64 rows of 466 columns, a KECCAK row 32 rows of 2,612 columns and 100 memory rows. |
| maximum constraint degree | 8 | Set by the blow-up above; each table’s degree is pinned by a test so that a new rule cannot silently exceed it. |
| tiers | 10, 12, 14, 16, 18, 20 | Tables are sized in six fixed steps, each four times the last, and a proof reveals only its tier: a coarse, padded bucket of the run’s length rather than its cycle count. Every bundle is tier 14 and every auth proof tier 10; calls are capped at tier 14 since fullnode v0.5.6. |
| maximum trace | 2²⁴ rows (2²⁵ with the zero-knowledge bit) | The size the extension-field term is proven for; a longer trace is refused rather than proved under a soundness claim that does not hold. |
| public values | 35 field elements | Entry pc, tier, eight output words, the three eight-word digests hc, H_IN, H_PUB, and the declared gas limit: the smallest statement that names the program, the private input, the public input and a bound on the work. It was 34 through constraint set 7. |
| proof size cap | 2 MiB default; 6 MiB on chain 21, 4 MiB on chains 17 to 20 (max_proof_bytes) | 2 MiB is above every proof the deployed guests make without a hash table and below a keccak-carrying one (≈ 3.3 MB); chains 17 to 20 set 4,194,304 to admit both, and chain 21 sets 6,291,456, room for the multisig auth proof (estimated at 3.4 MB or more; not yet measured). Chains 13 to 16 set 8 MiB. The cap is checked before a proof is decoded. |
What these numbers add up to: 86 bits of soundness proven, about 100 under the ethSTARK conjecture (where the extension field, not the queries, is the limit), and about 85 bits against a quantum adversary, set by the 256-bit transcript hash. The whitepaper keeps both soundness figures side by side on purpose: every deployed FRI-STARK relies on the conjecture for its last bits, and a reader sizing a deployment should know which assumption carries which bits.
Proofs about proofs
At about 1.50 MB a bundle proof is heavy, the 1.37 MB auth proof rides beside it, and every node stores them all. The remedy that is built is recursion: a proof whose statement is “I verified these proofs”. The fullnode carries a second, smaller machine for it, the recursion VM, whose native word is a Goldilocks element and whose instructions are the verifier’s own operations (field arithmetic, Poseidon2, Merkle and FRI steps). Its aggregate program replays the full verification of N bundle proofs and publishes a digest over their public values, bound to the chain, the aggregator and a nonce. Verifying one bundle proof inside it cost 1,968,619 rows under constraint set 6 and 2,044,506 under constraint set 7; it has not been re-measured under set 8 or 9. It is implemented and switched off by genesis on every chain so far, chain 21 included. The aggregate program verifies bundle proofs only, so on a split-authorisation chain the auth proof would stay. Its production proofs also need more memory than was planned for: measured under constraint set 7, 133 GB and more (Upcoming: block aggregation).
What the scheme does not claim
- The quantum margin is about 85 bits, not 128. Quantum collision search on an
n-bit hash costs about2ⁿ/³. The whitepaper’s design calls for a 384-bit transcript hash; the running chain uses the 256-bit Poseidon2 digest. Widening it is a new constraint set and a hard fork. - 100 bits is a conjecture; 86 is proven. Raising the proven figure to 100 would cost 97 queries, about a fifth more query data per proof.
- The Poseidon2 constants are a seeded draw, not a published instantiation. No attack on that is known; adopting a standard set would change every hash on the chain.
- Some statements are checked beside the proof, not in it. The
TIERpublic value is compared with the proof’s shape by the verifier but not constrained by the circuit (audit ZL1), and the bundle guest’s taint discipline is backed by structural argument, a weakened-guest harness and mutation fuzzing rather than machine-checked. - A proof’s header is not hidden. The tier and the two hash-table heights are in the clear, so a call proof shows whether the call used
KECCAKorSHA256. A prover option that pads both tables in exists and no wallet sets it: it takes a tier-10 proof from about 1.3 MB to about 3.6 MB. - The
POSEIDON2syscall is not injective over variable-length input. It is kept as it is because every commitment, nullifier, root and program digest depends on it. No shipped guest usesPOSEIDON2_LENyet. - Proving is slow and done by the sender, or by a prover the sender pairs with: about 100 s and 5.7 GB on a laptop CPU for a bundle. The GPU kernels first ran on a GPU (an NVIDIA H100) on 2026-09-28 and matched the CPU reference; the wallet still proves on the CPU.
Where to read more
- The whitepaper’s section “The Zero-Knowledge Scheme: How a Rand Proof Works” (
randprotocol.tex) walks through the same seven ingredients with the soundness theorems. - The implementation paper (
randprotocol_implementation.tex) specifies the instruction set and syscalls, the bundle guest, the sponge and digests, every table and bus, the machine configuration and the recursion VM. - The code:
crates/randprotocol-zkvm/src/in the fullnode —emulator.rs,tables/,machine.rs,gas.rs,guests.rs,hash.rs,poseidon2_constants.rs.