Skip to content

Latest commit

 

History

History
739 lines (650 loc) · 55.1 KB

aggregation.md

File metadata and controls

739 lines (650 loc) · 55.1 KB

Block-level proof aggregation and the prover market

Status: built (2026-09-15, the aggregation-spec branch): the chain-side spec (docs/superpowers/specs/2026-09-15-block-aggregation.md) is implemented in full — the register, the nine-step admission, the subsidy and fee split, sealing and pruning, sealed-form sync, the RPC and CLI, and the chain-9 genesis tooling. The measured numbers are collected in §6; the text of §1–§4 below is the approved design as written, kept because the build followed it almost word for word.

The original status line, for the record: approved design as a starting point, not built (decided and approved 2026-09-12; the spec and plan build on §1–§4 and settle the open questions in §5). This is the remedy chosen in docs/block-space.md §6 for the 80-query proof size: a block carries one recursive proof for all of its bundles instead of one ~1.3 MB proof per bundle. Proof pruning was rejected because it makes a syncing node trust finality signatures for old history and so opens the long-range attack a proof-carrying chain does not have.

1. Two roles, two kinds of hardware

role hardware admitted by does paid by
proposer (HotStuff leader) CPU stake (docs/staking.md, the S2 register) orders transactions, verifies one aggregate proof per sealed block (~0.8 s cold, ~16 ms warm) the verification share of fees, as today
aggregator (prover) GPU; measured: ≥ 512 GB host memory for one production proof (§ "Before enabling aggregation", item 3) permissionless, a registered payout address proves one recursive STARK that verifies N bundle proofs; submits it for a sealing block the proving share of fees plus a block subsidy in newly minted RAND

Validators stay CPU-cheap, so a home node can still validate. GPU capital competes in its own market and is never a requirement for consensus. The two roles may be the same operator, but nothing in the protocol couples them.

2. The pipeline

Nobody proves an aggregate inside a 2 s slot: a single bundle proof takes ~100 s today and a recursive proof over a block is minutes of GPU. So aggregation is pipelined:

  1. Unaggregated blocks. Senders submit bundles as today. A block at height h orders them and validators verify each bundle proof (three per block fit the 4 MiB cap). Consensus and finality run on these blocks unchanged.
  2. Sealing. Aggregators watch the chain, take the bundles of a window of finalised blocks, and prove one aggregate. The aggregate is submitted to the network and included in a later block h + k as a sealing record: Aggregate { covers: [bundle digests], proof, payout, r }.
  3. Sealed history. Once a block's bundles are covered by a finalised aggregate, every node may drop the individual bundle proofs and keep the aggregate; the ~3 KB of public fields per transfer stay. History remains proof-carrying end to end, which is the whole point.

The forward path (throughput above three per block) is the second step of the same design: bundles travel sender → aggregator, the aggregator's proof enters the block, and the individual proofs never do. That needs the aggregator to be on the critical path of inclusion, so it comes after the sealing pipeline has run on the fleet.

3. Payment

3.1 Fees split

A bundle's fee (docs/fees.md) today prices one verification and goes to the proposer. With an aggregator it prices proving too. The fee splits into a verification share (proposer) and a proving share (the aggregator whose aggregate first covers the bundle). A sender who attaches more than the floor is aggregated first; that is the fee-ordered mempool the block-space doc asks for, with GPU operators doing the ordering.

Under the genesis fees.burn_base (docs/fees.md §1.3) the verification share is destroyed instead of paid: the proposer keeps nothing at inclusion, BUNDLE_BASE joins burned and base_fees_burned, and the proving share — fee − BUNDLE_BASE, bucketed exactly as without the flag — reaches the covering aggregator, or the recorded proposer at the sweep, as it always has. Under fees.burn_floor beside it (issue #135) the whole floor is destroyed instead — a Deploy's per-word term and a Call's tier-exact gas and byte terms with the base — and the proving share is fee − floor, so an aggregator is never paid a priced term the chain burned.

Under the genesis fees.proposer_share_bps (docs/fees.md §1.3, docs/compute-optimization.md §6.2; aggregating chains only) the base itself splits: the proposer keeps proposer_share_bps / 10 000 of the base it keeps today (BUNDLE_BASE, nothing under burn_base) and the remainder joins the proving share in the bundle's one bucket entry — covered, it pays the aggregator; expired, the sweep returns it to the proposer like any excess. Under fees.prove_base (§6.3) every bundle's floor rises by prove_base, which is bucketed whole and never burned — and the node's pool prices with it too (Pooled.floor, current_floor), so it is never read as surplus and a call priced out by it is not offered. At 4 000 bps and 0.0006 RAND a 0.0018 RAND transfer pays the proposer 0.0004 at inclusion and buckets 0.0014.

3.2 A block subsidy in new RAND

At launch fee volume is near zero and nobody runs a GPU for it. So the sealing block mints a subsidy for the aggregate it includes, the way a Bitcoin coinbase pays a miner before fees matter, on a decaying schedule (halving on a fixed block count) that hands over to fees as volume grows. This is the chain's first issuance; today supply is genesis allocation plus faucet mints only.

What the subsidy is not, and why the design differs from proof-of-work:

  • The work is useful and bounded by demand. Hashing is wasted by design and scales with price; proving scales with the number of bundles waiting. There is no difficulty and nothing to adjust it against. So the subsidy rewards coverage, not compute: a fixed amount per sealed block, paid to one aggregate.
  • Per block, never per bundle. A per-bundle subsidy would let a prover farm it by filling blocks with its own transfers. Per block, filling does not raise the reward; it only helps win the selection.
  • Most coverage wins, not first. A first-valid race hands every block to the fastest GPU. Instead the sealing proposer picks, among the valid aggregates it received in the block's window, the one covering the most bundles; ties go to the lowest proof hash. A slower prover with a fuller aggregate stays competitive.
  • Security is not what the subsidy buys. Consensus security comes from bonded stake; the subsidy buys throughput and bootstraps the prover set. It can be small and can decay to zero.

A dollar-indexed schedule (genesis fees.usd_subsidy, docs/fees.md §1.3). A RAND amount buys a different number of GPU-hours at every price; the fee study's provers stayed for dollars. Under the sub-section the schedule of a sealed block is min(⌊usd_micros_per_sealed_block · 10⁹ / price⌋, max_subsidy_per_block) while the validator set's voted RAND/USD price is fresh (at most price_max_age_blocks old at the sealing block), and subsidy(n) otherwise — aggregation::schedule_subsidy, the one function admission, apply, rand_getEmission and the daemon share. The price is a vote (SetRandPrice, docs/staking.md "The RAND price vote"), not an oracle; the cap is genesis, not votable. Everything below — how the payment lands, the netting, supply accounting — reads "the schedule" and is unchanged.

3.3 How the payment lands

The subsidy and the proving share are paid as one deposit note to the aggregator's shielded payout address, exactly like a validator's Withdraw: the amount is public in that block, the aggregate carries the note's blinding r, and the ledger derives the commitment itself, so an aggregator cannot mint more than the schedule says. The note's later spend is unlinkable as any other.

Under the genesis fees.subsidy_net_of_fees (docs/fees.md §1.3) the shares pay the schedule first (spec §5.4's derivation, Ledger::aggregate_payment): the note carries max(subsidy(n), shares), and only subsidy(n) − shares — nothing once the shares reach the schedule — is minted. Under fees.usd_subsidy read the dollar schedule for subsidy(n) wherever it appears here (§3.2): the netting is the same function over whichever schedule applies. subsidised and rand_getAggregate's subsidy carry that minted part; sealed_blocks advances by one either way. Admission and apply still derive one note from one state: the rule lives in that one function.

An aggregator must net the subsidy too. The ledger never reads the envelope's amount: it derives the commitment from its own max(subsidy(n), shares), admits the aggregate and appends that note whatever the envelope says. An envelope sealed at subsidy(n) + shares therefore opens to a commitment matching no leaf, and the payout is lost to the wallet that holds it. The flag is read off rand_getLimits.fee_rules.subsidy_net_of_fees (null — no rule on — a reply that predates the section and a node with no rand_getLimits at all are the old sum). rand-node aggregate reads it through its client's cached limits (RpcClient::fee_rules, the same one rand_getLimits read the envelope format comes from: the section is fixed at genesis, so one read per daemon, never one per pass that a transient RPC failure could abort — issue #132), takes rand_status.aggregation's schedule beside it, and seals at aggregation::minted_subsidy(subsidy(n), shares, &fees) + shares, the ledger's own function (pinned by the_aggregate_pass_seals_the_ledgers_payout_under_subsidy_net_of_fees). A third-party aggregator must do the same.

And the dollar schedule. Under fees.usd_subsidy the daemon reads the voted price off rand_getRandPrice after the prove, beside rand_status, and seals at aggregation::schedule_subsidy over fee_rules.usd_subsidy and that price when the node reports it fresh — judged for the next block, the height the note's time names and the earliest the aggregate lands — and at subsidy(n) clamped to max_subsidy_per_block when it does not (pinned by the_aggregate_pass_seals_the_ledgers_payout_under_a_voted_price).

The payout is signed, and a moved schedule is refused, not underpaid (review 2026-10-09). Action::Aggregate carries payout_total, the amount the envelope was sealed for, inside the aggregator's signature (aggregate_signing_hash, domain rand-aggregate-4; genesis-bound rand-aggregate-5). validate_aggregate — admission and apply alike — refuses an aggregate whose payout_total is not aggregate_payment(..).total on its state, AggregationError::PayoutMismatch { expected, got }: a state verdict, never in the admission cache. So the schedule moving between sealing and inclusion — a SetRandPrice (in any earlier block, or in the aggregate's own: a proposer applies ordinary transactions before aggregates), the price going stale, sealed_blocks crossing a halving (the case that predates the dollar subsidy) — refuses the aggregate; nothing leaves the bucket and nothing is minted. The pool re-asks at every tip (check_aggregate_payout) and evicts a pooled aggregate whose payout moved. rand-node aggregate re-seals — the proof binds (chain, aggregator, nonce) and the covers, never the payout, so it is reused — and resubmits, up to four times a pass, after a PayoutMismatch refusal or an eviction. An unknown status alone is not an eviction: the node prunes its pool against the tip, the certified but uncommitted block, so an aggregate inside such a block answers unknown until the three-chain rule commits it — the daemon keeps polling, takes the committed register nonce moving past the one it signed as the commit, and re-seals only after 200 unknown polls (a minute) with that nonce unspent. Before this an aggregate landing across such an edge was paid the ledger's amount into a note its envelope could not open: subsidy and covered shares lost. (payout_total moved the Aggregate encoding and every aggregate txid; no chain has carried one.)

The shares are always the node's bucket entries, rand_getUnsealed's excess — never recomputed from a raw transaction. Under fees.proposer_share_bps and fees.prove_base an entry already carries the aggregator's part of the base and prove_base beside the tip, so nothing in what the daemon seals changes (pinned by the_aggregate_pass_seals_shares_that_include_prove_base_and_the_base_part); a share re-derived as fee − BUNDLE_BASE would miss the base part and seal a note the ledger never appends.

3.4 Supply accounting

docs/supply.md's value-balance invariant gains a term:

supply = genesis notes + faucet mints + Σ subsidies − burns

rand_getSupply reports issuance separately from faucet mints so an auditor can check the schedule against the sealed-block count.

3.5 Who made the proof: the aggregate binding (audit v3, AGG-2)

The payout goes to the aggregator that signed the Aggregate transaction. The proof itself must therefore say who made it; otherwise a registered aggregator could copy another's valid aggregate from the pool, re-sign it under its own identity and nonce, and be paid for the work. The rVM's aggregate program absorbs eight binding words into its interface digest:

interface = [inner_vk_digest(4) ‖ N ‖ B(8) ‖ 34·N public values]     -- pre-constraint-set-8

As of constraint set 8 (chain 18's pv::NUM = 35, docs/confidential.md's "Constraint set 8", built on feat/gas-chain18, not yet cut) the width is 35·N — a chain built after chain 18 carries [vk ‖ N ‖ B(8) ‖ 35·N], not the 34·N this section was written against. No live chain runs aggregation, and chain 18's genesis rejects gas.dynamic beside an aggregation section (docs/fees.md §1.2), so the two have never yet had to coexist.

B = aggregate_binding(chain_id, aggregator, nonce)   // H("rand-aggregate-bind-1", …), 8 LE u32

The prover (aggregate --watch) reads its register nonce before proving and binds its own (chain, address, nonce). Admission step 8 recomputes B from the transaction, never from the proof, and a copy of the proof under any other triple fails: re-signed by another aggregator, or replayed at another nonce. aggregate_binding is in randprotocol-core/src/types/actions.rs, and the program change is circuits 573ef2e. Because the program digest changed, a chain's admitted_shapes[].aggregate_program_digest must be measured on a build that carries it, and the production proof batch must use this program.

3.6 No slashing: a retry is not equivocation (INTERFACE-1)

SlashAggregator is refused on every aggregating chain (AggregationError::SlashingRetired, permanent). The register nonce advances only when an aggregate commits, so every retry after a lost race, a refusal or censorship re-signs the same nonce over new content — the --watch daemon draws a fresh r and time each pass — and the spec's equivocation proof was exactly two such headers, available to anyone. Exactly one aggregate can consume a nonce, so equivocation harmed nothing; the bond is what prices spam. The action's wire variant stays (its bincode index is part of every txid); slashed stays in the supply identity at 0.

3.7 The daemon: a failed pass is retried

rand-node aggregate runs one pass — aggregate_pass (the work list, the raw bundles, the prove, the signed aggregate) and the submission — and exits; with --watch it loops the pass on --interval-secs (15). The chain id is read once at start; from the second pass on, each pass first re-reads rand_chainId and compares. A node restarted onto another chain during a prove therefore surfaces one pass later: that pass's aggregate is refused at the mempool (a retryable failure), and the next pass's check stops the daemon.

In --watch mode a failed pass is logged at warn with its error chain and the daemon sleeps its interval and tries again: an RPC failure or malformed reply, a key not yet in the aggregator register (logged as "not registered yet (pending register?)": a daemon started right after aggregator register was submitted recovers once it commits), the register nonce moving while proving (§3.5: the pass is abandoned, since its proof can never verify), a failed prove, a refusal at the mempool, a commit that does not arrive within the wait. A sealed or empty pass resets the count of consecutive failures; at --watch-max-failures — default 20 consecutive failed passes, at least five minutes at the default interval (a hanging node also costs the read timeout per call) — the daemon exits non-zero with an error naming the cap and the last failure, so a permanently broken setup (a key that never registers among them) does not spin. An idle chain never reads the register, so an unregistered key on it fails nothing until work appears. The stop class — errors no retry cures, one enum (StopError in crates/randprotocol-node/src/main.rs) — exits on the pass it occurs in: the node now serves another chain id than the daemon started on (a chain restart behind the same RPC; the key must be registered on the new chain), and the node reports no binding_domain for a chain cut after it (BIND-1, require_binding_domain; a genesis cannot change while the daemon runs). A bad key file is read once, before the first pass. Without --watch a failed pass is the command's error, exactly as before.

3.8 Trees (tree aggregation, circuits recursion/docs/08-tree-aggregation.md)

A flat aggregate verifies at most N ≤ 5 bundle proofs at production (§"Before enabling aggregation", the reduce chip's N ceiling). A tree goes past it at a constant per-step cost: rv32n leaves of L bundle proofs each (today's aggregate program at N = L), then d levels of rv32t, the 2-to-1 interior step, whose child shape is compiled in and whose child key is a published tape value. Its root covers exactly L·2^d bundles. Built on circuits 92b750e (branch feat/tree-aggregation; re-pinned at 4a244b6, the final review's test and docs fixes, no src/ change), vendored into crates/randprotocol-rvm (2026-10-09). Like every aggregation rule it is genesis-gated and on no chain.

The genesis section. aggregation_tree (top level, beside aggregation, which it requires): the admitted shape it aggregates, leaf_size (L), max_depth, the leaf's rVM heights (leaf) and vk_leaf, and a per-level steps list — steps[k − 1] is level k's step: its rVM heights (tier, reg/ram/poseidon2/reduce_log_height), its program digest and the key digest of its proofs. The last entry repeats for every deeper level, which is sound only when it is a fixed point (the step built over its own proofs' shape is itself; R3). The genesis hash appends "aggregation_tree" ‖ bincode(section) after multisig, only when the section is set, so no existing genesis hash moves. TreeConfig::check bounds it: 1 ≤ L ≤ 5, 1 ≤ max_depth ≤ 8, a non-empty list no longer than max_depth, an admitted shape, no zero pin.

The cover cap is the layout's. A flat aggregate is bounded by aggregation.max_covers (unchanged, and ≤ the rVM's N ceiling, rand-node genesis's check_admitted_covers); a tree by its own section, L·2^max_depth, whatever max_covers says.

The layout rule. An Aggregate carries layout: flat | tree (a trailing 4-byte enum index on the wire, serde(default) flat). A tree covers exactly L·2^d bundles with 1 ≤ d ≤ max_depth; any other count is TreeLayout (or TreeDepth past max_depth), refused at preflight before the register is read or the signature checked. A smaller set takes the flat path. The layout is signed: rand-aggregate-tree-1 (chain-id form) / rand-aggregate-tree-2 (genesis-bound), the flat preimage under another tag, so flipping it is BadSignature.

Admission, a tree's steps. After step 6 (the covered shapes): 6t the section must exist for the covered shape (TreeNotAdmitted) and the count must be a tree (tree_depth). 7a (AGG-3 on the root's exact tier, R6): the root's declared tier, read off its header alone, must equal the tier pinned for level d (TreeTier) — nothing is built first. 7b (AGG-6 per level): the executor rebuilds the leaf and every listed step program and key from the pinned heights (AggExecutor::built_tree, memoised), and the root's program must be level d's pinned digest (AggregateProgramMismatch), then vk_leaf and each step key a depth-d recompute uses must be the pins (TreeKeyPin { level }, numbered by the level that consumes the key: vk_leaf is level 1, steps[k − 1].key level k + 1). A build that cannot rebuild the section at all is TreeRebuild (the startup check refuses to start on such a section, so a running node does not reach it). 8 verify_tree: first the root's other four declared heights against level d's pins, off the header (aggregate_header_tree, before any key is built), then the bottom-up recompute from the covered bundles' public values in cover order — leaf i's digest is the flat bound list [inner_vk ‖ L ‖ B ‖ 35·L] over its L runs, and each level j hashes adjacent pairs as [vk_c ‖ 2 ‖ B ‖ D_a ‖ D_b] with vk_c = vk_leaf at level 1 and steps[j − 2].key above it (the last repeating) — compared with the root's four public values (TreeRootDigest), then Machine::verify_n(root program, root, 2), which holds the root's reduce height to the canonical one. A covered run that cannot be a bundle's (an OUT word past u32, a non-canonical word) is CoveredPublicValues { index }, before the recompute.

ZKQ-5, implemented. There is no in-program B_out == B_in: a step does not check that its children were made under its own binding. Admission recomputes every level from the covered bundles under the transaction's one binding B (§3.5), so a root whose any level was made under another binding — another aggregator's, another nonce — digests differently and is TreeRootDigest. Swapped children, a foreign leaf program, a flat proof presented as a tree and a wrong-shape child are refused the same way or in-program (circuits tests/tree.rs).

The errors, all AggregationError: TreeNotAdmitted, TreeLayout { covers, leaf_size }, TreeDepth { depth, max }, TreeTier { declared, pinned }, TreeKeyPin { level }, CoveredPublicValues { index }, TreeRootDigest, plus the flat path's AggregateProgramMismatch for the root's program, and TreeRebuild when this build cannot rebuild the section. Every one but TreeKeyPin and TreeRebuild is a verdict on the bytes against genesis constants and is cached by the admission refusal cache (admission::is_permanent); those two stay out, as AggregateProgramMismatch does (a build/genesis disagreement, not the transaction's fault).

At the cut. rand-node genesis --aggregation-tree <TREE.JSON> writes the section after the genesis bounds and this build's check_tree_pins (below) both pass; nothing is written otherwise. TREE.JSON is the TreeConfig JSON — every digest four canonical u64 limbs, as JSON numbers:

{
  "shape": { "profile": "Production", "tier": 14, "program_log_height": …, "input_log_height": …,
             "keccak_log_height": …, "sha256_log_height": …, "public_log_height": …, "mem_log_height": … },
  "leaf_size": 2, "max_depth": 2,
  "leaf": { "tier": …, "reg_log_height": …, "ram_log_height": …, "poseidon2_log_height": …, "reduce_log_height": … },
  "vk_leaf": [u64, u64, u64, u64],
  "steps": [ { "heights": { … }, "program_digest": [u64, u64, u64, u64], "key": [u64, u64, u64, u64] }, … ]
}

circuits production_tree_genesis_values prints exactly this between its == aggregation_tree markers (save that block as TREE.JSON); the spec's names map onto it as step_tiers[k] = steps[k].heights.tier, step_digests[k] = steps[k].program_digest, step_keys[k] = steps[k].key. The test tree's section, in the same form, is circuits recursion/tests/tree-test-genesis.json (vendored at crates/randprotocol-rvm/tests/tree-test-genesis.json; agg_executor's the_printed_tree_section_is_a_tree_config deserializes it). rand_status.aggregation.tree is the same section for humans, its digests as 64-character hex (the four limbs, 16 hex digits each, in order — a limb past 2^53 would lose bits in a JavaScript reader); rpc::TreeStatus::to_config converts it back, and --aggregation-tree takes only the limb form.

At startup. A node whose genesis carries the section rebuilds it and compares every pin — each listed level's program digest and key, and vk_leaf, whatever depth a tree may later take — before it opens its store (agg_executor::check_tree_pins, after the disk guard, on a blocking thread that startup waits on): aggregation_tree pins: <error>; refusing to start on a mismatch, aggregation_tree pins: ok in the log otherwise. It also refuses a section the rVM could never serve: a leaf size past the rVM's N ceiling for the shape (production N ≤ 5), a pinned reduce height that is not canonical for its program (the leaf at N = L, every step at N = 2), and a max_depth past the listed levels whose last entry is not a fixed point. The warm thread then builds one rVM key per listed level, the root's at that level's tier and canonical height (warm_tree). Today this path is behind the pre-existing refusal of every aggregation genesis (below).

The RPC and the daemon. rand_status.aggregation.tree is the section, its digests as 64-character hex strings (absent without one). rand-node aggregate --layout tree reads it once, checks that its own build reproduces every pin (check_tree_pins, the node's startup check — refused before any proving otherwise), takes L·2^d covers for the largest d ≤ max_depth the unsealed work list fills (nothing if fewer than 2L are unsealed), proves the tree on one machine (randprotocol_rvm::aggregate::aggregate_tree: leaves, then each level, every proof checked against its pinned shape before the next level is built on it) and signs under the tree domain. --layout flat (the default) is today's daemon.

The test-profile values (circuits docs/08 §5; the node's suite rebuilds and checks them, agg_executor::tests): leaf rv32n, L = 1, tier 18, heights 19/19/14/16; level 1 rv32t_leaf tier 19, 20/20/15/16; level 2 rv32t_int tier 19, 20/20/15/17 (the reduce table crosses 2^16: the step is not its own fixed point); the fixed point is reached at level 3 (rv32t at the root's shape). vk_leaf 188d4b14…acfdcb, vk_t_leaf 6b042444…d8507a, vk_int a92a0baa…3efbb70, vk_fix 4203726c…7e6e355; programs T_LEAF_TEST 2c586636…206ccd34, T_INT_TEST 04b16771…dc3c226, T_FIX_TEST 08abf188…795ff396. A test-profile step proves at ≈ 34 GB maximum RSS on a 48 GB laptop. Production values: pending (droplet; circuits docs/08 §4) — the production depth-2 tree (L = 2, tier-21 steps, ≈ 122–197 GB projected at rate ¼, the 256 GB droplet) has not been proved, so no production section, digest or host class is recorded here yet.

Before enabling aggregation

Aggregation does not remove the auth proof (audit v6, AGG-7; recorded 2026-09-30). On a split-authorisation chain (hc_auth set: chains 17 and 18) a bundle carries two proofs: the tier-14 bundle proof (~1.49 MB) and the tier-10 auth proof (~1.36 MB). The aggregate program verifies bundle proofs only, and the pruned form replaces only Bundle.proof (Transaction::hash's doc comment: "The pruned form replaces only proof, never auth_proof"). So sealing a window shrinks a transfer from ~2.85 MB to ~1.36 MB, not to nothing, and a node syncing sealed history still verifies every auth proof itself. Either the aggregate program is extended to verify the auth proof too, so the pruned form can replace both — a program change, with the digest re-measured and re-pinned — or storage is planned on 1.36 MB a transfer. Decide before aggregation is switched on.

Aggregation is off on every live chain, and rand-node refuses to start on any genesis that carries an aggregation section (node::check_build_runs_genesis). Two reasons hold it there; both have to clear before a genesis may switch it on.

  1. The hidden-asset bundle's shape. The admitted shapes and the recursion fixtures were measured for the retired 2-in-2-out guest; they are re-measured for the current bundle guest, and admitted_shapes[].aggregate_program_digest is taken on the build that will run.

  2. The rVM's own soundness (2026-09-27). The recursion-VM security report's RVM-1 let a prover choose the high lane (column D1) of every extension value a STOREE writes to memory — no register read bound it — and the aggregate verifier stores 14 370 of them per inner proof (the REDUCE descriptors, the register allocator's spills). The same day's zk scan added the reduce chip's free per-row clock (OPCODES-1/TABLES-1), its padding rows that could write (V-OPCODES-1), runs that could stop before their write-back (ZKR-4), the public table's optional rows (OPCODES-4) and the unchecked base addresses and r31 pairs (ZKQ-3). All are fixed in circuits b786aae and cd4b788..971b96b (on circuits main after the memo commit c6cdef4), vendored here at 971b96b, each with a red-first cheating vector. The aggregate program's digest does not change with any of them (a pinned test says so); the rVM verifier does, so a proof from an unfixed prover no longer verifies. What has not been done, and has to be before a genesis enables aggregation:

    • An end-to-end forged-aggregate exercise against the fixed verifier — run 2026-09-30 (issue #45, closed; circuits feat/issue-45 d38d6cf) on a 503 GB machine. A malicious inner proof never reaches the prover, and a forged stored lane inside a real tier-19 aggregate's trace is refused both ways it was built. With the RVM-1 fix reverted (scratch copy, never committed) the isolating vectors accept the forgery (the forged run VERIFIED … publishing [12648430, 11, 11, 22]), so they have teeth; the aggregate-scale test is stopped by RAM read-after-write instead, and a lane chosen to cancel a failing final check (the report's §6 path) was not built — issue #119.
    • The report's §11, what its review did not cover — rechecked on 2026-09-28 by the zk scan's "not covered" pass, which found no soundness break in any of them:
      • the generated constraint evaluation (programs/constraints.rs) against Plonky3 0.7.0's verifier folder, term by term (fold order, selectors, quotient recomposition, LogUp terminals, X² = 7): no mismatch. The Production-profile differential (all 9 instances) and a test that breaks a single AIR constraint on a non-first instance now exist and pass (#45);
      • the rv32n absorb schedule ([vk ‖ N ‖ B ‖ 34·N] at the time of this review, 35·N since constraint set 8 — the same schedule, re-checked at the new width by circuits 18c2627's fix for the sponge's block-boundary bug, docs/confidential.md's "Constraint set 8"), the length in capacity, the final permutation) and hostile cover sets (N = 0, a wrong N, duplicates, mixed shapes): no desynchronisation — read, not fuzzed. ZKQ-5, decided 2026-09-28 and implemented for trees in circuits docs/08: no in-program B_out == B_in; admission recomputes every level from the covered bundles under the transaction's one binding (§3.8 Trees);
      • the CUDA and reference backends: no verifier path differs under their features;
      • the witness tape and host-side replay: admission read every cover before checking the count and signature (ZKQ-1, fixed in this release);
      • timestamps: proven collision-free at every tier (16·CLK + slot ≤ 2^27, integral CLK on every real row once the reduce clock is carried).
    • Deferred from the zk scan (ZKQ-6): a DSL-side check that a loop body never reads a register before writing it.
  3. The hardware (measured 2026-09-30, #45). A production aggregate of one bundle proof (tier 21) peaked at 376.9 GB and took 8 131 s; N = 2 (tier 22, 2.4 % row headroom) is estimated at ~750 GB and N = 3 at over 1 TB — beyond any single CPU host, with or without the GPU backend (host-resident traces). An aggregator therefore needs ≥ 512 GB for N = 1, and N ≥ 2 needs a design change first (#119). Production N = 2 and N = 3 emulation (no proving) is within the memory and timestamp bounds (2^27), and register pressure does not grow with N. The full table is docs/node-hardware.md §4. Phase 2 row cuts (circuits 75b7893, 2026-10-04): the production inner proof is 893 606 cpu rows, so N = 1 lands at tier 20 (N = 2 at 21, N = 3/4 at 22) and projects to ≈ 240 GB — a ≥ 256 GB host, tight, and a projection until proved. On that tree admitted_shapes[].aggregate_program_digest is c90b3f0a…74d8 and the node admits tiers {20, 21, 22} (recursion/docs/04-phase2-row-cuts.md, docs/node-hardware.md §4). rVM phase 3 and rate ¼ (circuits 71e1a04, vendored 2026-10-08; recursion/docs/06-phase3-fold-reduce.md, docs/07-rvm-rate-quarter.md): the production inner proof is 585 686 cpu rows (still tier 20) and admitted_shapes[].aggregate_program_digest at the production bundle shape is dc350ecf6b60af74f4bb032bdf607c3fa0fbd6317705f0b1077e71b455e38ba0 (was c90b3f0a…74d8; phase 3 moved it, rate ¼ did not; inner_vk_digest and the interface vectors are unchanged). Tiers stay {20, 21, 22} production (N = 1 at 20, N = 2–3 at 21, N = 4–5 at 22) and {18, 19, 20} test. rVM phase 4 and constraint set 9 (circuits 57fb9dc, vendored at d217252, 2026-10-11; recursion/docs/09-phase4-spans.md): Merkle spans (opcodes 25/27 now take an absolute address in their 4th word, under an unchanged key) bring the production inner proof to 315 391 rows, so N = 1 lands at tier 19 and the node admitted tiers {19, 20, 21} production ({18, 19, 20} test); the MODQ chip then changed the inner shape. Current pins (agg_executor.rs's tests): production admitted_shapes[].aggregate_program_digest 8bc2cecbd30b975ec15a5e11354434bd7e1c2814d46dd0acc1d92b3ff944d8c7 (dc350ecf…8ba0 → e0eede8b…b6c3 at phase 4 → 8bc2cecb…d8c7 with the MODQ chip) and inner_vk_digest 1b0c4b0043dd5d00d710f10048bfde989a955034d7fd52f1a85595a61339ddc7 (moved for the first time since constraint set 8; the admission vectors were re-proved with it). Every earlier value in this list is superseded; crates/randprotocol-node/tests/no_pre_phase4_recursion_pins.rs refuses the stale full literals in code. No modq-bearing inner proof reaches the chip-free recursion verifier (docs/zkvm.md §8a). The reduce chip runs 196 960 rows a production proof (39 392 test) with the MODQ chip; N ≤ 5 unchanged (5 × 196 960 + 1 ≤ 2^20); tier 22 admits nothing. rVM phase 5 (circuits b3e21d9, vendored 2026-10-15; recursion/docs/10-phase5-n2-tier19.md): four program cuts and a new QueryBits tape layout, no ISA, AIR or key change, bring one inner verification to 235 418 rows: production N = 1 is 235 744 rows at tier 18, N = 2 471 105 at 19, N = 3 706 466 at 20, N = 5 at 21, so the node admits {18, 19, 20, 21} production ({18, 19, 20} test). Current pins: production admitted_shapes[].aggregate_program_digest 792a66d4a995c7b3874d38ecb8c971d5494b57b4cce13db11e08a4f9b2f9a982 (8bc2cecb…d8c7 before), verify_rv32.digest c3065887…1b24; inner_vk_digest and the admission vectors are unchanged. The test tree's rv32t_int digest and step keys wait on circuits' fixture regeneration (docs/10 §5).

    • The N ceiling: production N ≤ 5. The reduce chip runs a fixed 196 960 rows a production inner proof (39 392 test; 196 480 / 39 296 before the MODQ chip) and REDUCE_MAX_LOG_HEIGHT is 20, so an aggregate verifies only while N × rows + 1 ≤ 2^20: N ≤ 5 production, N ≤ 26 test. Tiers {18, 19, 20, 21} are admitted (tier 22 is refused, agg_executor::admitted_tiers), and the ceiling inside them is the reduce chip's. The rVM refuses to prove past it, and the node mirrors the rVM's own machine::max_reduce_n: verify_aggregate refuses an aggregate over more bundles by name before any key work (agg_executor::check_n_ceiling), and rand-node genesis refuses an --aggregation whose max_covers exceeds it for an admitted shape (check_admitted_covers). Raising it is a key-shape change (one more rung of the constant doubles the range).
    • The reduce height is canonical in (program, N). The node verifies through the rVM's verify_aggregate, which passes the list's N to Machine::verify_n; any declared reduce height but canonical_reduce_log_height(program, N) is refused before a key is built (the key-cache DoS guard; it replaces the node's own reduce-flag check). warm_aggregation warms one key per admitted tier and canonical height for N = 1..=max_covers — production heights 2^18 / 2^19 / 2^20 at N = 1 / 2 / 3–5, so a max_covers of 3 is 3 tiers × 3 heights. At rate ¼ the production startup warm therefore grows from 3 to up to 9 verifier-key builds, off the consensus thread; its timing is not yet measured at rate ¼.
    • fri_profile binds two parameter sets. A chain's fri_profile name fixes the inner RV32 proofs' FRI parameters (80 queries, rate ⅛, 20 grinding bits) and the rVM's own (92 queries, rate ¼, 24 grinding bits at production), both constants of the vendored constraint set, not genesis fields; the proven floor is the same ≈ 86 bits (over the rVM's real chip shapes, under the whitepaper's unique-decoding bound: 86.41 at 80 / rate ⅛ / 20, 86.38 at 92 / rate ¼ / 24 — circuits docs/07 §1).
    • Every node on this build. A chain with an aggregation section needs every node on this build or later: the rVM verifier keys (the reduce chip's preprocessed region, the rate-¼ profile) and the aggregate program digest changed, so an older node refuses every aggregate this build accepts and pins a digest no current program produces. No genesis in deploy/genesis-chain*.json (chains 6–20, the newest 20) has an aggregation section, and deploy/cut-chain21-genesis.sh refuses one, so no chain is affected today.

Testing: the recursion fixtures (issue #131)

The node's fixture-backed tests (agg_executor::, the covered-assembly tests in node::, the aggregation tests in rpc::, storage::seal_tests::) read real bundle proofs through fixture_proof(k), <dir>/Test-{k}.proof. The set is exactly Test-0, Test-1, Test-2: every literal caller uses k = 0, and the_admission_recompute_reproduces_the_pinned_vectors_byte_for_byte reads Test-1 and Test-2. The pinned set is committed, crates/randprotocol-node/fixtures/recursion (~1 MB), and is fixture_proof's default directory; RECURSION_FIXTURES overrides it. So the suite needs no setup (2026-10-06, no env var: 491 passed; 0 failed; 1 ignored). A missing file is a panic, not a skip, by design.

cargo test -p randprotocol-node --lib                           # reads the in-repo set
scripts/recursion-fixtures.sh --check [<dir>]                   # check a set (default: the same dir)
scripts/recursion-fixtures.sh <circuits checkout> [<dir>]       # re-prove (a re-pin), then check

Why these bytes. A fixture's notes are random, and the pinned-vectors test pins the 118-word interface list and digest (3534960f…) that one set of notes produces: the cache re-proved 2026-10-03 for the phase-2 re-vendor, which is what is committed. A freshly proved set passes every other fixture-backed test and fails that one at "the pinned 118-word interface list" (measured 2026-10-06: 490 passed; 1 failed; its inner_vk_digest assertion, the data-independent half, passes). The script compares each file's sha256 with PINNED_SHA256 and exits 0 when the set is the pinned bytes, 1 when a file is missing (or the checkout is refused, or a generator failed), 3 when the set is complete but not the pinned bytes.

Re-pinning, when the vendored zkVM moves. The fixtures are proofs by circuits' zkVM (research/ and the bundle guest in guests-compiled/), so a re-pin proves from a checkout whose two trees are CIRCUITS_PIN in .github/workflows/ci.yml — the script refuses any other (recursion/, the outer rVM that only hosts the generator, may drift). A set from another constraint set fails the suite with "cs8 proofs carry pv::NUM public values" (the pre-cs8 cache under circuits/recursion/target/recursion-fixtures does). The script runs circuits' recursion/tests/fixtures.rs generator once per k in parallel (FIXTURE_PROFILE=Test FIXTURE_KS=k), which skips a fixture that still verifies and re-proves one that does not; measured (2026-10-06, M4 Max): Test-1 and Test-2 proved in parallel in 4 min 36 s wall, ~4.6 min a proof. The new files then end at exit 3; in the same commit re-measure the test's constants and circuits' recursion/docs/02-aggregate.md on them and move PINNED_SHA256.

CI. CI runs these tests (since 2026-10-08). check-and-test and nightly-proving run scripts/recursion-fixtures.sh --check first, so a missing or edited fixture fails by name before the suite, then the library suites read the in-repo set; scripts/ci-fixture-skips.sh now skips only the rVM's two long proving tests, by full name (a_one_proof_aggregate_round_trips_…, two_test_profile_bundle_proofs_…: tier 18 at N = 1 and tier 19 at N = 2, tens of GB; the N = 2 one is also #[ignore]d in source). That closes PROC-7's open half for the node crate. (Until then the skip list removed these tests on the grounds that a clean runner had no cache, which stopped being true when the set was committed.)

4. Fallback

A block whose bundles no aggregate ever covers stays valid: its raw bundle proofs are kept and three per block is the rate. No subsidy is paid for it; fees go to the proposer as today. The chain never depends on a GPU being online.

5. Open questions for the spec

  • The subsidy amount, the halving interval, and whether issuance has a hard cap. Built, genesis-gated: fees.usd_subsidy indexes the amount to a dollar target at a voted RAND/USD price, bounded per block by a genesis cap (docs/fees.md §1.3); the halving schedule remains the fallback.
  • The proving share: a fixed fraction of the bundle fee, or the whole fee above the floor. Built, genesis-gated: fees.proposer_share_bps buckets a fraction of the base beside the whole excess, and fees.prove_base floors it (docs/fees.md §1.3; the proposal's values are 4 000 bps and 0.0006 RAND).
  • The sealing window k: how many blocks an aggregate may lag, and whether a bundle can be covered twice (it should not; the first finalised aggregate wins, later ones covering it are invalid).
  • Submission spam: verifying an invalid aggregate costs ~0.8 s. Either aggregators register with a refundable bond and sign submissions, or nodes verify submissions off the consensus loop with a per-peer rate limit (the same machinery the RPC hardening task adds for bundles).
  • The verifier guest: the STARK verifier (Poseidon2 Merkle paths, FRI folding) in RISC-V, its cycle count per inner 80-query proof, and the tier it lands in. This is the zkVM milestone the rest depends on.

6. Measured numbers (2026-09-15, this tree)

Every number below was measured on the implementation branch, not projected. The test-profile rows are the cluster capstone's (tests/cluster.rs's a_fresh_node_syncs_pruned_history_with_one_rvm_verify_per_sealed_window); the production rows are the activation checklist's placeholders (docs/deploy.md, "Chain 9 activation"), filled at activation.

measurement value source
bundle proof (register), test profile, tier 14 94.3–99.1 s across runs, 321 701–327 322 bytes (random notes vary the witness) capstone register stage
aggregate prove, test profile, N=1 (tier 19) 1568.2 s wall (~26 min) on a loaded shared box, 327 035 bytes capstone aggregate stage
startup key-build, test profile (2¹⁹) 13.0–13.4 s (the ops expectation at 2²¹ production: ~30–70 s) warm_aggregation startup log, three nodes
warm aggregate verify, test profile ~1–2 s (the first at a shape pays the 13 s key-build, once) admission step 8
seal → pruned sealed at block 46; the pass at head 64 rewrote the record (every 16 blocks, gated at sealed_at + window) capstone
sealed resync, a fresh joiner 1 rVM verification per sealed window (the covering aggregate's; a raw sync re-verifies each bundle); the 7-block sealed batch applied in under a second capstone's verification counter
the capstone end to end 1873.2 s (register 97 s → prove 1568 s → seal → prune → resync) tests/cluster.rs
rVM aggregate proof size, test profile 325–327 KB measured here (the circuits M5.3 record is 328 121 bytes; production measured 2026-09-30 at 1 563 226 bytes for N=1 under constraint set 8 — under the 2 MiB default cap, not the ~0.5–0.6 MB first estimated) circuits/recursion/docs/02-aggregate.md
tree, test profile (circuits docs/08 §5, 2026-10-09, 48 GB laptop) leaf rv32n L = 1: 169 640 rows, tier 18; step rv32t_leaf: 287 107 rows, tier 19, 280 133–280 772 bytes, 34.35 GB max RSS, 177 s; root rv32t_int: 306 985 rows, tier 19, 283 172 bytes, 33.21 GB, 182 s circuits recursion/tests/tree.rs, docs/measurements/2026-10-09-tree-test-fixtures-laptop.log
tree, production (circuits docs/08 §3) step rv32t_leaf at the production leaf shape 1 294 577 rows, tier 21, emulated; proved leaf/step/root rows, memory and times pending (droplet) circuits recursion/tests/tree_measure.rs
the interface conformance vectors inner_vk_digest 33a94ec6…92a1c8, the 115-word bound list (AGG-2; the stand-in binding of circuits/recursion/docs/02-aggregate.md), digest 9833ac5b…fdc868e — reproduced byte-for-byte by the fullnode's recompute the conformance suite (agg_executor.rs)

The one capstone walk-through

Register an aggregator (one bond-burning bundle, proving share 7 over the floor) → the chain halts for the prove (the cover window cannot scroll) → one rVM aggregate over the bundle's proof (the proving slot's single hold) → submitted, admitted (its own verification), pooled per §3.4's selection, committed — the sealing block — the mark lands atomically with the commit → the prune pass rewrites the record (34 public values + the 7 declared shape bytes, ~3.3 KB against ~1.3 MB raw) → a fresh node joins and syncs the pruned block in sealed form, reaching the same blocks and state roots at every height with exactly one rVM verification — and the supply audit holds end to end (total_supply == issued − slashed, with one subsidy minted against sealed_blocks == 1).

What the capstone exposed: the sealed-sync stall

The capstone passed alone, twice (1873.2 s, 1876.4 s), and stalled inside the full cluster suite three times — the fresh joiner stuck part-way with nothing outstanding and nothing counted as failed. The mechanism, once the warn-level keeper diagnostics named it, was two give-ups compounding:

  • A batch cut between a pruned block and its cover is unservable, deterministically. The sync client's batch count halves on wire failures (under suite load, toward 1) and the server's byte budget cuts where it cuts; either can end a batch after a sealed block (the window at 33) but before its covering aggregate (block 46). The joiner's coverage check refuses the batch — correctly, it is batch-atomic — and the raw-form fallback asked another peer, which pruned the same record and served the same split form. A give-up that can never succeed, retried forever.
  • The sync peer picker had no fallback. When the fresh peer's connection flapped or its status had not landed, the picker returned nothing and the sync stopped silently until gossip happened to re-trigger it; a send that failed was not even counted.

The fixes, each with its node-lib regression test:

  • Coverage-closed serving (node.rs's close_batch_coverage): before a Blocks response goes out, every served pruned entry's seal mark is resolved and the batch extends — past the count asked, past the soft byte budget — until every cover it needs is in. The extension's only ceiling is the reader's own wire limit; a chain whose cover sits beyond that is the genuine archive case the raw-form fallback exists for. Pinned by a_batch_cut_short_of_its_cover_extends_until_the_coverage_closes (the count cut), a_batch_cut_by_bytes_short_of_its_cover_extends_within_the_reader_limit (the byte cut), and the_extension_stops_at_the_reader_limit_and_serves_what_it_can (the ceiling).
  • The robust picker (node.rs's pick_sync_peer): the freshest connected peer ahead wins, but when the chain is known ahead and no connected-and-fresh pair exists, any connected peer is worth one round trip — a possibly-stale answer costs a little, a silent stall costs the chain. A send that cannot go out warns, counts, and tries the next candidate. Pinned by pick_sync_peer_prefers_fresh_and_falls_back_to_any_connected.

What pruning shrinks, and how much work a pass does (INTERFACE-9, issue #51)

A committed transaction is stored twice: inside its block's CF_BLOCKS row, and as its own CF_TXS record. Until issue #51 the pruning pass rewrote only the record, so the block row kept the raw ~1.3 MB proof and the disk §6.2 promises back never came back; and the pass found its work by walking every seal mark the store had ever written, every 16 blocks, for ever. Both are fixed node-side (no consensus, wire or genesis change; dormant while no chain carries an aggregation section):

  • The block row shrinks with the record, in the same synced batch (Storage::stage_pruned): the transaction at the record's index becomes the record's marker form. That is exactly the row a block synced in sealed form has always been stored as (commit stores a block "as served"), so no reader meets anything new, and both copies now hold one form:
    • Serving never used the raw row for a pruned bundle — sealed_form_of serves the record's marker form and side entry whichever row it finds — so a Blocks response is byte for byte what it was, and coverage-closed serving (close_batch_coverage, above) reads only the seal marks. What the raw row could have offered, a raw proof to a syncer whose fallback wants one, this node never served; that fallback was and is an archive's job (--keep-raw-proofs, which never runs the pass).
    • The startup replay meets the marker and applies the block through the pruned branch with the side table rebuilt from the records (INTERFACE-3, the sealed-synced path): each bundle's public fields are bound to its record's OUT and H_PUB words, and the covering aggregate's block, replayed after it, verifies the rVM proof over those same records. That is the soundness a sealed-synced node already rests on, and the one §6.2 prunes for: once a window has passed its seal, the aggregate — not the bundle's own STARK — is the proof.
    • The block hash cannot move: the marker form hashes to the raw hash (the proof enters the id by digest, rand-txid-2), and the write refuses unless the slot's hash, the record's hash and the block's tx root all still agree.
    • Visible changes: rand_getBlockByHeight/ByHash report a pruned bundle's proof_len as the marker's 44 bytes, as rand_getTransaction already did off the record; a by-hash block fetch of such a block returns the marker form, as a sealed-synced node's always has — by-hash fetches serve pending and recently committed blocks, never history a window past its seal.
  • A height index for the pass: CF_SEALS rows h ‖ sealed_at (BE) ‖ bundle, written with every seal mark. The pass walks them from the lowest sealed_at, stops at the first still in its window, deletes each row it handles, and handles at most PRUNE_SEALED_PASS_MAX (64) a pass — steady state is 16 × max_covers, so the cap only paces a backlog. Its work is what came due since the last pass, never the sealed history. History pruning and truncation drop a mark's index row with the mark.
  • Upgrade: a store sealed by an older build is indexed once at open (seal_heights_built); the next passes then also shrink the block rows an older pass left raw behind already-pruned records. An older build ignores h rows and replays marker rows (INTERFACE-3), so a rollback needs nothing.

Pinned by the_sealed_pruning_pass_visits_only_the_marks_whose_window_passed_once, the_sealed_pruning_pass_drains_a_backlog_a_bounded_step_at_a_time, a_store_sealed_before_the_height_index_gets_it_at_open, history_pruning_and_truncation_drop_the_height_index_rows, a_pruned_bundle_shrinks_its_block_row_too_and_the_startup_replay_still_verifies, a_block_row_left_raw_behind_a_pruned_record_shrinks_on_the_next_pass (storage.rs) and a_block_row_this_node_pruned_serves_coverage_closed_like_a_sealed_synced_one (node.rs) — all stub-proved, so they run without a recursion fixture.

The fleet bundle's declared shape

The admitted shape chain 9 registers is the fleet's own measured classes for a 2-in/2-out bundle — {tier, program 12, input 10, keccak 0, sha256 0, public 2, mem 16} — and NOT the recursion fixtures' shape (13/12/18): the same guests::bundle() underlies both, but the chain executor pins the tight declared classes (ZkExecutor::bundle_heights) while the fixtures' auto-derived ones land a rung or two looser, and a declared height is part of the shape. The cluster capstone pins this distinction by asserting the committed bundle's header equals the admitted shape; a genesis cut with the fixtures' classes would admit a shape no fleet bundle can ever match.

Re-measure before activating (2026-09-19). That shape's public 2 is the empty public segment's height. Since the transaction binding (docs/confidential.md, "Transaction binding") every bundle proof carries eight binding words and declares public 4, so no bundle proved after that fork matches the chain-9 shape. Aggregation is inactive on every chain and chain 14 is cut without it; re-measure the admitted shape (and the recursion fixtures) before any chain is cut with an aggregation section.

Enforced at genesis (2026-09-28, IFACE-9). Genesis::validate now refuses an admitted shape that is not the bundle header every accepted bundle carries — tier 14, no keccak or sha256 table, public height 4 (BUNDLE_PROOF_TIER, BUNDLE_PUBLIC_LOG_HEIGHT in core, pinned to the zkVM's own numbers by an agg_executor test) — and rand-node genesis refuses one the rVM cannot build an inner verifier key for (InnerShape::try_of, via agg_executor::check_admitted_shape). deploy/cut-chain9-genesis.sh's default public height is 4 accordingly; the program, input and mem heights are still the re-measurement above.

Related: docs/block-space.md (the numbers and the three remedies), docs/fees.md, docs/supply.md, docs/staking.md, docs/zkvm.md.