Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root - #598
Open
johnchandlerburnham wants to merge 2 commits into
Open
Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root#598johnchandlerburnham wants to merge 2 commits into
CheckEnv claims, and verify one root#598johnchandlerburnham wants to merge 2 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR implements the aggregate-first path for Aiur Multi-STARK proofs. It
lifts independently-proven IxVM shards into one recursion system, folds their
CheckEnvstatements along the manifest's bisection tree, and persists onerecursive root proof.
The fold has two join modes:
join_twoperforms a canonical set union/difference and is efficient forsmall lower nodes.
join_two_structuralcommits tonodeHash(leftRoot, rightRoot)in one hashand discharges assumptions with Merkle inclusion paths. Its subject work is
independent of subtree size, avoiding the top-of-tree re-rooting blowup.
The CLI selects the modes monotonically with
--structural-above N(default4096 subject leaves): flat below the threshold, structural above it.
This is the stage-2 aggregation layer from
plans/aggregate-first-pipeline.md: shard proofs are aggregated before anyterminal KZG/SP1 compression, so a later wrapper only needs to consume one root
proof. Terminal compression remains outside this PR.
What lands
verify_multi_stark_proof,join_two, andjoin_two_structural.transitive verifying-key, function-index, and allowed-system binding.
CheckEnvdecoding and canonical flat set folding.keeping output assumption sets canonical and deduplicated.
entrypoint indices.
.ixesaggregation-tree parsing and post-order lowering, with abalanced fallback for legacy manifests.
--structural-above.both join modes.
ix aggregate --ixe E --ixes M [--structural-above N] <proof>..., includinga cheap
--plan-onlymode.and manifest-bound hybrid-root modes.
malformed-input, and semantic-tampering tests.
Protocol design
One recursion system, three entrypoints
MultiStark.multiStarkprunes the combined call closures of:verify_multi_stark_proof: the existing lift, which verifies one prooffrom an arbitrary digest-bound Multi-STARK system;
join_two: the flat join, which opens canonical subject trees and foldstheir sorted sets; and
join_two_structural: the structural join, which treats child subjectroots as opaque Merkle roots and combines them with one node hash.
All three share one recursion verifying key. Every child outer claim identifies
its entrypoint by function index, and both join entrypoints constrain that index
to one of these three allowed forms. Verifying under the same key is therefore
not enough to admit an unrelated recursion function.
Public statement and pinned protocol identity
Both joins expose the same public-input shape:
Each value is a 32-byte Blake3 digest packed injectively into eight
little-endian four-byte Goldilocks values. The 16-element public input commits
to:
CheckEnvclaim.The allowed blob is:
The circuit requires exactly 96 bytes and rejects the old 88-byte layout. The
entrypoint indices must be explicit because Source DSL programs cannot
materialize their compiler-assigned function indices. Binding the verifying
keys alone would permit a claim about any function in those systems; binding
all four relevant indices pins the complete recursive protocol identity.
The host and root verifier independently compile both systems and reconstruct
this blob. No identity is accepted from the persisted proof wrapper on trust.
Child-proof validation and transitive pinning
Each join reads the recursion verifying key once, checks its Blake3 digest
against the allowed blob, deserializes it strictly, and uses it for both
children. For each child it:
ood_verify, including Fiat-Shamir replay, lookup accumulators,quotient/OOD checks, Merkle openings, and FRI verification; and
The accepted forms are:
blake3(ixvm_vk). Its claimsdigest opens to exactly one IxVM claim at the pinned
verify_claim_idx, whosedigest opens to a strict serialized
CheckEnvclaim.current
allowed_digest, transitively pinning the same systems and allentrypoints. Its second digest opens to its output
CheckEnvclaim.Any other index, shape, digest, trailing byte, or failed proof check rejects.
This keeps lift→join, flat→join, and structural→structural composition on the
same statement format.
Flat canonical folding
For child statements
(S_L, A_L)and(S_R, A_R),join_twoenforces:Every present tree is strictly parsed and fully consumed. Its real leaves must
be nonempty and strictly byte-lexicographically increasing, simultaneously
enforcing sorting and deduplication. The circuit recomputes the canonical
Blake3 Merkle root, including zero-address padding for odd levels, then checks
the union and difference equations with linear sorted merges.
Address order is constrained over eight big-endian
u32words using thefull-domain
u32_less_thanprimitive. The implementation never uses Aiurpointer identity as address equality: distinct pointers imply distinct
allocations, not distinct stored byte strings.
A flat parent does not trust a structural child's opaque root. It can consume
that child only if advice opens the root as a valid canonical sorted tree. A
genuinely free-form structural root therefore rejects. In the harmless
shape-coincident case where a structural root also has a canonical opening,
the circuit proves that opening rather than relying on its provenance. The
monotone host scheduler avoids needing such openings in normal operation.
Structural root and assumption discharge
join_two_structuralperforms the same allowed-blob binding, recursion-keybinding, two child-proof verifications, child decoding, and output-claim
binding as the flat entrypoint. It then replaces all subject-tree loading and
set re-rooting with:
Input and output assumption trees remain canonical because they represent a
small sorted frontier. The circuit walks the sorted, deduplicated union of the
two input assumption lists. For every candidate, channel 6 supplies exactly
one choice:
0): no bytes may follow, and the candidate must be the nextvalue in the output assumption list; or
1): a bounded Merkle path fromleafHash(candidate)toS_out.rootmust verify.Path payloads are strict:
countis limited to 64.side = 0hashesnodeHash(sibling, current)andside = 1hashesnodeHash(current, sibling); any other side rejects. Thepayload must be fully consumed and the folded root must equal the output root.
The candidate walk prevents dropping an assumption without proving discharge,
and it requires the output list to be exhausted, preventing extra assumptions.
Survivors do not need non-membership proofs: carrying a candidate that is also
present in subjects only weakens the statement and prevents the desired
unconditional root. Duplicate subject leaves in a structural forest are also
safe because discharge needs only one valid membership path.
Advice and FFI contract
Both joins use the same seven-channel IO layout. Digest/root bindings are
checked before the corresponding bytes are decoded.
[0],[1][0][0],[1],[2]CheckEnvclaim[0]Lean passes channels 4–6 across FFI as compact framed blobs:
The Rust decoder borrows payloads rather than cloning them. It rejects
truncation, offset overflow, impossible entry counts before allocation,
declared-length overruns, and trailing bytes before circuit execution.
Flat joins receive full subject/assumption tree advice and an empty path blob.
Structural joins receive only the input/output assumption trees plus one path
choice for every deduplicated candidate; subject trees are never opened.
Manifest and host pipeline
.ixesaggregation treeparseIxesManifestexposes both shard block lists and the optional binaryaggregation tree stored at the manifest tail. It:
tree tail or with an explicit absent-tree tag.
The tree lowers to post-order
FoldOpslots. Every join refers only to earlierslots and the last slot is the root, providing a direct serial schedule and a
stable basis for future parallel/cache execution.
Monotone structural scheduling
The host counts actual owned constants for each shard after the disjoint-cover
check.
schedulePlanannotates every slot with its cumulative subject count andchooses a structural join exactly when:
The default threshold is 4096;
--structural-above 0makes every joinstructural for testing. Nonempty children make counts strictly increase toward
the root, so once a structural node appears, all ancestors are structural.
This prevents a flat parent from needing to open a free-form structural child.
The proving loop checks reconstructed subject counts against the schedule
before starting expensive work.
ix aggregateThe command:
shard cover;
CheckEnvclaim and canonical trees;then requires exact claim equality and one proof per shard;
allowed blob;
that they reproduce the environment's canonical root;
proof at its scheduled root entrypoint; and
Ixon.Proofcontaining the outputCheckEnvclaim and proof.--plan-onlystops after coverage validation and scheduling. It needs no proofarguments and does not compile recursion systems or begin proving:
ix verify --aggregateAggregate roots reuse
Ixon.Proof, so verification is selected explicitly:Without environment inputs, the verifier tries the flat and structural root
entrypoints and reports which one verified.
--ixealone retains the all-flatcanonical-root behavior. With both
--ixeand--ixes, it rechecks shardcoverage, reconstructs every leaf statement, reruns the same threshold
schedule, folds the exact hybrid root, requires no assumptions, and verifies
under the scheduled root entrypoint. The threshold must match the proving run.
Existing non-aggregate proof and shard-composition verification paths are
unchanged.
Native execution, proving, and generated code
Large advice values remain raw byte blobs across the Lean/Rust boundary. This
PR adds or extends:
Bytecode.Toplevel.executeMultiStarkJoinfor generated or interpreted flatand structural join execution;
AiurSystem.proveMultiStarkJoinfor witness generation and proving;JoinAdvice,JoinPreimage,JoinTree,JoinPath, strict framed decoders,and the seven-channel
join_io_buffer; andrs_aiur_multi_stark_join_executeandrs_aiur_multi_stark_join_proveexterns.The generated executor is parity-checked against the bytecode interpreter on
both output and every circuit's unique-row/total-hit counts. The combined
production verifier now contains 247 Aiur functions; the regenerated Rust file
is 2,221,559 bytes. Most line churn is generated code. The protocol source of
truth is
Ix/MultiStark/Aggregate.lean; the native advice contract lives inaiur_multi_stark_runner.rs.Test coverage
The
aggregate-firstsuite uses a small stand-in child system so it can createreal Multi-STARK child proofs without the tens to hundreds of GiB required by a
production lift. Those proofs are consumed by the production join circuits, so
proof validation, statement decoding, set/path folding, native advice, and
transitive recursion are exercised end-to-end at execution time.
The 29 passing aggregate-first cases cover:
The primitive Multi-STARK and recursive-verifier suites were also rerun to
guard the shared verifier and lift paths.
Validation run
All of the following pass on this branch:
A real two-shard manifest from the 5,986-constant
tc-parity.ixeenvironmentalso passes coverage and structural scheduling with threshold zero:
Soundness summary
join, and nested protocol identity is pinned transitively.
bytes.
every unique input assumption through either inclusion or explicit carry.
noncanonical assumption tree rejects.
the CLI requires an unconditional final root.
verification independently reconstructs the scheduled hybrid claim.
Review map
Ix/MultiStark/Aggregate.lean,Ix/MultiStark.leanIx/MultiStark/Host.leanIx/Cli/CheckCmd.lean,Ix/Cli/AggregateCmd.leanIx/Cli/VerifyCmd.leanIx/Aiur/Protocol.lean,Ix/Aiur/Semantics/BytecodeFfi.lean,crates/ffi/src/aiur/protocol.rscrates/ixvm-codegen/src/aiur_multi_stark_runner.rscrates/ixvm-codegen/src/aiur_multi_stark.rsix codegen --checkTests/MultiStark.leanCurrent limits and non-goals
measurements project roughly 195 GiB peak RAM for even the small production
lift, while the development host has 124 GB. The real end-to-end gate needs a
host with at least 256 GiB RAM.
measure structural path totals, prove time, peak RSS, or proof size.
therefore restricted to all-flat roots; hybrid roots should be verified with
both
--ixeand--ixesand the same threshold used during proving.content-addressed, but resumable caching and level-parallel joins are
follow-ups.
and empty-leaf pruning need an explicit persisted root-kind/system format.
Ixon.Proofand do not encode the proof system;callers must pass
ix verify --aggregate.or padded with dummy calls. This matters before a future static terminal
circuit consumes the verifier.
multiproofs/cold-circuit grouping, add production benchmark rows, or
implement a terminal KZG/SP1 wrapper.
Follow-ups
and both join modes' time, peak RSS, proof size, and native verification.
Mathlib scale.
explicit RAM budget.
selected terminal compression path.