accumulation and the nox machine — 2026-10-09
Work package F of the proof-system repair (soft3
proposals/proof-system-repair.md §3 "unbounded programs", §4, §5 phase 3).
What was built, what it measures against the phase-3 gates, and what is not
done. Contracts: specs/machine.md, specs/accumulation.md,
specs/soundness.md § machine proofs and accumulation.
Stand: zheng feat/accumulation (rebased onto docs/proof-repair-ledger, PR #50, itself on #49), lens
feat/accumulation (on perf/batched-merkle aee14d9), joy
feat/accumulation (on feat/zk-and-state-binding, joy #36); hemera
perf/permutation-neon, nox / trident release/0.4 unchanged. Machine: Apple
M4 Max, 16 cores, 48 GB, macOS 26.4, rustc 1.95.0, --release, no LTO.
The machine was shared with other agents during every run (load average
17–58); times are single runs under that load and are upper bounds of a
quiet machine. Sizes are deterministic.
1. what was built
| piece | where | status |
|---|---|---|
| uniform step relation for nox | rs/src/machine/ |
built, tested (§4); one fixed row relation, 64 + 15 columns, degree 8, ~430 constraints; the verifier key is the constraint structure, a program enters through public columns and five constants |
| AIR proving over segments | rs/src/air/ |
built: zerocheck over rows, successor polynomial, public columns, shared lookup challenges across segments, boundary rows |
| accumulation of RS evaluation claims + decider | rs/src/accumulate/ |
built: WARP's claim-carrying structure for RS codes (not ARC verbatim — §5) |
| fold of N statements of one CCS relation | accumulate::ccs |
built: 512 statements → one decider |
| envelope profile 4 (machine) | rs/src/envelope/machine.rs |
built |
| lens: WHIR over Fp3 messages, leaf access | lens rspcs/src/whir/access.rs |
built (base commitments, transcripts and bytes unchanged) |
joy prove --succinct falls back to the machine |
joy rs/machine_execution.rs |
built |
recursion verify(verify(π)) |
— | not built (§7) |
2. columns of the tables
cycles = nox's charged cost (nox::reduce budget spent); rows = trace
rows (init + machine steps + padding + permutation blocks) as
segments × 2^n; proof B = MachineProof::to_bytes (AIR messages of
every segment + one accumulation step per segment + the decider; the
envelope adds the statement and an 8-byte header); air, acc, decider =
its three parts; prove = AIR + accumulation + decider (excludes the native
run); verify = machine::verify from the parsed proof (median of 3).
3. measurements
shipped WHIR parameters (rate 1/64, k = 4, grinding 24, Johnson)
Command: ZHENG_TIMING=1 ZHENG_SEG=16 machine_bench add.tri hash.tri merkle-32.
| fixture | cycles | rows | proof B | air | acc | decider | prove ms | verify ms |
|---|---|---|---|---|---|---|---|---|
| add.tri (7, 5) | 33 | 1 × 2^8 | 83,408 | 6,760 | 32,658 | 43,977 | 55,788 | 6.67 |
| hash.tri (7) | 61 | 1 × 2^10 | 96,968 | 7,296 | 37,042 | 52,617 | 49,322 | 5.43 |
| merkle-32 (32-hash path) | 1,121 | 1 × 2^15 | 145,500 | 8,636 | 49,762 | 87,089 | 184,786 | 8.64 |
Verifier parts (ZHENG_TIMING, medians of 3): hash.tri AIR 0.8 ms, the
accumulation step 1.6 ms, the decider 3.1 ms; merkle-32 AIR 1.0 ms, step
2.0 ms, decider 4.6 ms. Prover: grinding (24 bits per query round and per
WHIR fold) dominates; the AIR is under a second at hash.tri. Prove times in
this table were taken at load 40–60 and vary ×3 between runs. For reference, the one-relation succinct profile proves hash.tri in
15,921 B and verifies in 7.96 ms (succinct-profile-2026-10.md §4).
long runs (rate 1/16, k = 4, grinding 24, Johnson; segments of 2^14 rows)
Command: ZHENG_TIMING=1 ZHENG_RATE=4 machine_bench tree-1 tree-8 tree-12
and … rec-16. tree-k: a balanced tree of 2^k additions (4·2^k − 1
cycles); rec-n: a self-referencing doubling recursion f(n) = 2^n over
~2^{n+1} computed-formula calls (a program of tens of tokens).
| fixture | N = cycles | rows | proof B | air | acc | decider | prove ms | verify ms |
|---|---|---|---|---|---|---|---|---|
| tree-1 | 7 | 1 × 2^7 | 93,284 | 6,492 | 36,258 | 50,521 | 46,993 | 4.97 |
| tree-8 | 1,023 | 1 × 2^12 | 140,924 | 7,832 | 53,266 | 79,813 | 76,452 | 11.33 |
| tree-12 | 16,383 | 3 × 2^14 | 383,938 | 25,096 | 266,248 | 92,581 | 160,347 | 40.15 |
| rec-16 | 1,572,850 | 241 × 2^14 | 26,949,860 | 2,015,728 | 24,842,210 | 91,909 | 5,914,223 | 3,927.30 |
rec-16 on the post-review code (75f3846, rerun 2026-10-09, load 7–72,
machine-opcodes-2026-10.md): AIR 1,028 s, 241 steps 4,831 s, decider
55 s, peak RSS 7.2 GB — it completes. The first attempt below is kept for
the record. rec-16 (1.57M cycles, 3,948,544 rows) was started on the pre-review code
(before fresh-word OOD answers): the AIR of all 241 segments took 1,483 s
(24.7 min, 3.0 GB resident); its 241 accumulation steps were still running
at the time of writing (log /tmp/wpf/bench_rec16.log on the stand machine).
No size or verify time is claimed for it. Per step measured on tree-12 (an
earlier run, load ~20): AIR 5.2 s, one accumulation step 14.1 s; the
decider once, 33.7 s. Per segment the proof grows by the
segment's AIR messages (~8.3 KB) and its accumulation step (~88 KB: three
words — the accumulator and the segment's two — each opened at t = 53
positions with Merkle paths).
the accumulator
AccConfig at grinding 24 (accumulate::tests::config_table):
| rate | ℓ | regime | t | s | claims | instance as encoded | compressible to |
|---|---|---|---|---|---|---|---|
| 1/16 | 20 | Johnson(19) | 53 | 1 | 55 | 27,977 B | 2,068 B |
| 1/16 | 21 | Johnson(19) | 53 | 1 | 55 | 29,297 B | 2,092 B |
| 1/64 | 20 | Johnson(7) | 36 | 1 | 38 | 19,341 B | 1,592 B |
| 1/64 | 21 | Johnson(7) | 36 | 1 | 38 | 20,253 B | 1,616 B |
The accumulator is constant in the number of steps: one root, 1 + s + t
claims. It is never sent in a proof (the verifier derives it); "compressible"
counts root, ρ, values, OOD points and t symbol indices instead of
pow(x) points — the encoding a PCD would ship.
512 statements of one relation into one decider
execution::succinct::fold_tests: joy's hash.tri on inputs 0..511, each
statement's Spartan IOP down to its witness claim (committed free half,
ℓ = 10), accumulated 64 words per step (8 steps), one decider. Test
parameters (rate 1/8, grinding 16):
| total B | per-statement claim | 8 steps | decider | verify ms |
|---|---|---|---|---|
| 8,906,708 | 2,748 | 7,464,447 | 35,277 | 1,171 |
Against 512 independent succinct proofs (~16 KB each at the shipped parameters, ~8.2 MB): one WHIR opening instead of 512, the same order of bytes — the per-word spot checks of the accumulation steps are what a verifier reads.
joy
joy prove --succinct on a 600-iteration trident loop (x = x·x + 1; the
relation compiler refuses its nesting) falls back to the machine: 13,825
reductions, 455,141 B envelope, 366 s (shipped parameters, load ~50),
joy verify 36 ms; a wrong claim and wrong inputs are refused. The CI test
(140 iterations) proves and verifies in 120 s.
4. soundness tests
- 20 single-opcode programs, joy's compiled hash.tri (inputs 7, 0, p−1) and
add.tri ((7,5), (0,0), (p−1,3)), the 11-hash chain, Merkle-8, tree and rec
programs: output noun and cycles equal
nox::reduce's; every constraint holds on every row (single segment) —machine::tests,tests/machine.rs; - every native failure class (atom formula, unknown opcode, add on a pair, axis through an atom, inverse of zero, branch on a pair, budget one short) is a machine failure;
- tampered result atom, cycle count, frame id, permutation state and init entry violate the relation;
- forged input, cycles, output, program; a lying Merkle root; swapped segments; weak/truncated/extended envelopes: rejected;
- accumulation: false input claim, forged openings, other roots, bit flips of a step, wrong accumulator claims and decider value: rejected;
- 512-statement fold: a statement with another output, a forged witness claim, a dropped step: rejected;
- an independent read-only review of the constraint system and the AIR protocol found no under-constrained cell, memory hole or control-flow escape (notes applied: a comment, the running-sum pin's role);
- bit-flip scan of a full machine envelope (add.tri, shipped parameters, after the review fixes): 83,593 bytes, 668,744 flips, 666,280 decoded, 0 accepted (385 s; before the fixes: 82,705 B, 661,640 flips, 0 accepted).
4b. review fixes
An independent soundness review (no break found) asked for: fresh words
bound by out-of-domain answers before any challenge they feed (AIR phase-1
and phase-2 words, CCS witnesses — done, air::commit_bound,
accumulate::fresh_ood); interactive vs ROM bounds separated in the ledger;
citations corrected (Lemma 4.13 argument with BCGM Def 3.14; OOD = WHIR Lemma
4.25); AccConfig refuses deciders below 128 bits and steps over
max_claims; the prover refuses a word whose data is not its instance; wire
docs fixed; lens serde round-trips Fp3 proofs; a domain string for CCS claim
proofs. Tests for each; sizes above are after these fixes.
5. which accumulation, and why
Implemented: WARP's claim-carrying structure (eprint 2025/753 §6
pseudo-batching, Construction 7.2 codeword batching) instantiated for
Reed–Solomon codes, with univariate out-of-domain samples as in ARC
(eprint 2024/1731 §2.1) and WHIR, the powers generator in one combination
round, and claims carried instead of WARP §8's constraint batching. Not ARC
verbatim: ARC's quotient step needs a per-step Fill message and FFT over
Fp3, its NP accumulation carries two oracles, and its stated Johnson-regime
proximity error is weaker than the mutual-correlated-agreement bound (BCGM
2025/2051 Lemma 9.3) lens already relies on; the claim-carrying form lets
the existing multilinear WHIR opening be the decider. The round-by-round
argument (specs/accumulation.md § soundness) composes the cited lemmas for
this protocol; it is mine, not a paper's theorem, and has not had an
external review.
6. against the gates (proposal §5, phase 3)
| gate | measured | verdict |
|---|---|---|
| merkle-32 proves | 145,500 B, verify 8.6 ms | proves; misses ≤ 64 KB (2.3×) |
| a 10^6-step run proves | rec-16, 1,572,850 cycles: native agreement and trace built (long_runs_agree_with_native_nox checks rec-14, 393,202 cycles); proved and verified on the post-review code: 26.9 MB, prove 98.6 min, verify 3.9 s (rerun 2026-10-09, machine-opcodes-2026-10.md) |
met (size and verify time far from the other gates) |
| size independent of length, ≤ 64 KB | 83 KB (33 cycles) … 146 KB (merkle-32) … 384 KB (16,383 cycles) — linear in the number of segments (~96 KB per 2^14-row segment) | missed |
| verify ≤ 1 ms | 5.4 ms (hash.tri) … 8.3–8.6 ms (merkle-32) … 40 ms (3 segments) | missed |
| bit-flip scan of a full accumulated proof: 0 accepted | 668,744 flips, 0 accepted | met |
verify(verify(π)) at depth 2 |
not built | missed (§7) |
| 512 tickets decide in one proof | 8.9 MB, verify 1.17 s, one decider | met as an API; foculus wiring is another package |
Why size is linear. Accumulation reduces N claims to one decider, but
each step's proof (spot checks in every folded word) must be checked by
someone. Without recursion the final verifier checks every step, so the
proof carries every step: bytes and verifier time are linear in the number
of segments. Proposal §4's "accumulation … no verifier circuit is ever
built … this is how 'any computation' is proven at constant size" does not
hold: constant size needs the accumulation verifier inside the next step's
relation (PCD, ARC §2.3 / WARP §1), which is recursion. Measured here: one
accumulation step costs ~88 KB of openings at ℓ = 20 (rate 1/16) — more
than the WHIR opening it replaces.
Why merkle-32 misses 64 KB. The trace is 2^15 × 64 cells: each level
re-derives the structural digest of two fresh digest nouns (4 atom digests
of 2 permutations + 3 pair digests each) plus the pair and the hash opcode —
~24 hemera permutations of 32 rows per level. The decider alone (WHIR over
an Fp3 word at ℓ = 21) is 87 KB; the accumulation step folding the two
committed words is 50 KB. Levers not taken: a narrower permutation trace
(perm rows use 22 of 64 columns), batched WHIR over both words instead of an
accumulation step, compressed accumulator encodings.
Why verify is ms, not µs. The machine AIR verifier itself is cheap (constraint evaluation once per segment); the time is hemera Merkle paths in the accumulation step and the decider (~5–8 ms per segment-step and decider at these parameters).
7. not done
- Recursion. No relation expresses the accumulation step's or the
decider's verifier, so there is no
verify(verify(π))fixture and no decider-as-relation. What it needs, measured against this stand: the lens transcript and Merkle leaves hash bytes (7 bytes per element), so every absorbed Fp3 element and every leaf symbol needs an in-relation byte decomposition (the machine's atom-digest bits rows are that gadget); Fp3 arithmetic rows; hemera node rows (the machine'sPAIRjob ishash_nodewithout the root flag); the WHIR schedule is static. One accumulation step atℓ = 20checks3 × 53leaves with ~15-level paths: ≈ 2,400 node permutations + ≈ 160 leaf sponges + ≈ 300 transcript permutations ≈ 3,000 permutations ≈ 96K permutation rows plus byte decompositions — a 2^17-row relation per step before the segment's own rows. Not started. - Constant proof size and ≤ 1 ms verification (follow from recursion).
- The 64 KB bound for merkle-32 (levers in §6).
- Opcodes lt, xor/and/not/shl, call, look; axis addresses ≥ 2^32 (refused,
never proven wrongly) — done on
feat/machine-opcodes,machine-opcodes-2026-10.md. - The arena capacity of
nox::Reduction<N>is not modelled; the budget equivalence relies on nox'sbound()being an upper bound (review checked the covered opcodes). - foculus wiring of
accumulate::ccs(another package); lattice-fold spike (proposal §B); zip. - A compressed wire form of an accumulator instance (only needed once accumulators travel, i.e. with PCD).