Continuations (Approach 2): prove executions epoch-by-epoch#685
Continuations (Approach 2): prove executions epoch-by-epoch#685nicole-graus wants to merge 65 commits into
Conversation
…orst-case local-to-global memory stress benchmark
… count by running the executor only, as a no-proving proxy for monolithic proving memory
… ordering constraint
…file instead of a fresh one
Keep the follow-up scoped to non-performance cleanup while preserving the soundness regression coverage. - Add L2G/global-memory regression tests for MU selector behavior, chain truncation, l2g-root binding, and private-input continuations. - Fix stale continuation/global-memory docs and comments. - Replace bare x254 byte address literals with register_base_address(254). - Remove the unused DEFAULT_EPOCH_SIZE constant and document run_epochs as a test/bench helper.
Replace the continuation CLI's raw --epoch-size / --num-epochs controls with --epoch-size-log2. The CLI now computes an exact power-of-two epoch size directly, defaults to 2^20, rejects tiny log2 values below 18, and no longer runs a cycle-count pre-pass to split into a target epoch count. Update the continuation design doc and help text with the ethrex 10-transfer memory sweep as guidance.
…er-fixed GENESIS_EPOCH constant
|
/review-ai |
Codex Code ReviewFound 2 issues in the PR diff:
Static review only; I did not build or run tests per the requested constraints. |
| is_first: bool, | ||
| is_final: bool, | ||
| ) -> VmAirs { | ||
| let register_init_arg = if is_first { None } else { Some(register_init) }; |
There was a problem hiding this comment.
register_init_arg (and therefore the is_first branch here) is dead. build_epoch_airs always passes register_preprocessed = Some(...) to VmAirs::new, and VmAirs::new only consults its register_init parameter in the else branch (when register_preprocessed is None). So this value is computed and threaded through but never used, and the if is_first wrongly implies epoch-0 register handling differs (it doesn't — genesis vs. carried init is already fully captured by the preprocessed commitment built from register_init). Suggest dropping register_init_arg/is_first from build_epoch_airs and passing None for VmAirs::new's register_init.
Review: Continuations (Approach 2)Reviewed the continuation prover/verifier, the L2G + GlobalMemory tables, the register/x254 cross-epoch binding, the Fiat-Shamir statement binding, paged_mem, and the executor/CLI wiring. This is a large, carefully-constructed change with strong documentation and an unusually good set of adversarial negative tests (dropped/reordered epochs, tampered reg_fini/l2g_root/private inputs, malformed lengths, inflated table counts, oversized inputs). Reviewed statically from the diff. Soundness — no issues foundThe main soundness arguments hold up:
Minor
Nothing blocking from a correctness/safety standpoint. |
AI ReviewPR #685 · 31 changed files
Findings
Status column reflects the verdict from the verifier: deepseek-verifier (openrouter/deepseek/deepseek-v4-pro). AI-001: Continuation proof bundle leaks private inputs in the clear
Claim ContinuationProof serializes the full Evidence
Suggested fix Do not ship raw private inputs in the self-contained proof bundle. Carry the precomputed private-input page commitments (or a single commitment) and have the verifier check the global-memory preprocessed columns against them, or explicitly document that continuation proofs reveal private inputs. AI-007: Continuation bundle deserialization lacks size limits on untrusted fields
Claim
Evidence
Suggested fix Add upfront size/length caps when deserializing or before processing ( AI-011: Dead code: register_init_arg and is_first in build_epoch_airs
Claim
Evidence continuation.rs:306 computes Suggested fix Either drop the Reviewer Lanes
Verification Lanes
Native Codex and Claude reviews run separately and post their own comments. They are not included in this structured provenance report. Discarded candidates (9) — rejected by the verifier
Raw lane outputs, candidates, final issues, and model metrics are uploaded as workflow artifacts. |
…734) The cross-epoch ordering check proves `init_epoch < fini_epoch` via an IsB20 (20-bit) lookup on `fini_epoch - 1 - init_epoch`, so a run can have at most 2^20 epochs. Beyond that the IsB20 bus cannot balance and no honest proof exists. Previously this was guarded only by a debug_assert in the prover's bitwise emission, so a release build would build an unprovable trace and fail cryptically — reachable via the library API with a small epoch size (the CLI's min epoch size keeps it out of reach there). Add a hard check in `prove_continuation`'s epoch loop returning `Error::InvalidContinuationEpochSize` with a clear message once the epoch count would exceed the range. This is a prover-side guard only: the verifier already rejects any such proof (the IsB20 table is preprocessed and the ordering sender is rebuilt verifier-side from a positional epoch label), so soundness is unchanged — it just turns a confusing failure into a clean error. Introduce `local_to_global::MAX_EPOCHS` as the single source of truth, used by both the new check and the existing debug_assert (replacing the `1 << 20` literal).
Motivation
Monolithic proving holds the whole execution trace in memory, so program size is bounded by RAM. Continuations split a long execution into fixed-size epochs that are proven independently, keeping peak memory flat as the program grows. Only memory and registers have to be linked across epoch boundaries.
What this does
GlobalMemoryLogUp plus aglobal_memorytable that anchors genesis (initial memory, recomputed from the ELF by the verifier — not prover-supplied) and finalization (final values).FINIcolumn (= the epoch's final register file) and reusing it as the next epoch's preprocessedINIT, soinit(epoch i+1) == fini(epoch i)by construction. Registers are few and fixed, so no telescoping/global bus is needed.x254(the COMMIT trace and the verifier's commit-bus offset both start from it), and the driver concatenates each epoch's committed bytes into the run-wide public output.Soundness mechanisms
init_epoch,init_timestamp) viaIsHalfwordhalfwords; value bytes viaAreBytes;address/fini_timestampneed no extra check (matched against MEMW, like PAGE).fini_epochas a verifier-supplied per-table constant (1-based labels, genesis = 0) — the prover can't choose it.init_epoch < fini_epoch, checked viaIsB20[fini_epoch − 1 − init_epoch], preventing a row from referencing a future/own epoch to inject an unjustified value.MUselector (1 on real rows, 0 on padding) gates every L2G interaction. Gating the epoch-local Memory bus is load-bearing:MU=0on a genuinely-touched cell dangles its MEMW tokens, so the epoch proof fails — which forcesMU=1on every touched cell, forces every touching epoch into the cross-epoch chain, and so makes the chain complete and the prover-supplied finalization trustworthy. (An earlier variant gatingMUonly on the GlobalMemory bus was unsound — a prover could orphan a touched epoch and drop a write — and was reverted.)FINIis a preprocessed column, so the verifier recomputes its commitment and the prover can't deviate from the public value; the Memory bus (REG-C2 ↔ MEMW) pins that value to the register's real last write. The same value is reused as the next epoch's preprocessedINIT.Full write-up, including the
MU-wiring designs, the register binding, the Fiat-Shamir binding, and the adversarial-review reasoning, indocs/continuations_design.md.Status — draft
Memory and register continuity, the cross-epoch commit index, and a standalone (split) prover/verifier are complete and bound.
prove_continuationemits a self-containedContinuationProof;verify_continuation(elf, &bundle)checks it using only the bundle and the ELF — it enumerates the epochs itself and turns the cross-epoch bindings into explicit verifier checks rather than values reused from the prover's memory: the register/x254 chain (epoch i+1's INIT derived from epoch i's bound FINI), the L2G commitment binding, ELF-genesis for both registers and memory, and the epoch label/count from a trusted enumeration;table_countsis validated and per-epoch page configs are not trusted. The public output is reconstructed from the per-epoch bound commit slices. The bundle serializes withbincode(like a monolithicVmProof) and is driven from the CLI (prove/verify --continuations). (Adversarially reviewed: register/x254 chain, L2G root binding, and completeness-by-enumeration each survived a construct-a-break audit.)Deferred:
Usage (CLI)
Continuations are driven through the existing
prove/verifycommands with--continuations(binary attarget/release/cli, orcargo run --release -p cli --):--epoch-size N— epoch length in cycles (rounded up to a power of two, min 4).--num-epochs K— split into K epochs (epoch_size = ceil(cycles / K)via a cycle pre-pass). Mutually exclusive with--epoch-size.--epoch-sizeis what keeps peak prover memory flat as the program grows; the epoch-count modes are convenience defaults.--blowup B— FRI blowup factor (power of two ≥ 2, default 2), same as the monolithic path. Security stays at 128-bit regardless (the FRI query count is derived from the blowup), so a higher blowup just trades more proving work for fewer queries. Applied to every epoch proof and the global proof, and must match betweenproveandverify.verify --continuationsneeds only the bundle + ELF (the bundle carries the private inputs). It prints the reconstructed output and exits non-zero if invalid.Example:
cli prove executor/program_artifacts/asm/test_commit_split.elf -o /tmp/cont.proof --continuations --num-epochs 2 cli verify /tmp/cont.proof executor/program_artifacts/asm/test_commit_split.elf --continuations # → Verification succeeded! Output: aabbccddHow to test