Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root - #598

Open
johnchandlerburnham wants to merge 24 commits into
mainfrom
jcb/aggregate-first
Open

Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root#598
johnchandlerburnham wants to merge 24 commits into
mainfrom
jcb/aggregate-first

Conversation

@johnchandlerburnham

@johnchandlerburnhamjohnchandlerburnham commented Aug 27, 2026

Copy link
Copy Markdown
Member

Summary

This PR implements the aggregate-first path for Aiur Multi-STARK proofs. The
production path now runs through one ix_aggr recursion entrypoint: it wraps
independently-proven IxVM shards, folds their CheckEnv statements along the
manifest's bisection tree, and persists one recursive root proof with a
uniform 18-word outer claim. Every completed recursive slot is persisted in a
verified, content-addressed version-2 resume cache, and ready slots are proven
in parallel under explicit job and RAM limits.

The entrypoint has two pair-fold modes:

  • Flat shapes perform a canonical set union/difference and are efficient for
    small lower nodes.
  • Structural shapes commit to nodeHash(leftRoot, rightRoot) in one hash and
    discharge assumptions with Merkle inclusion paths. Their 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 (default
4096 subject leaves): flat below the threshold, structural above it.

IxVM shard proofs
│
├── ix_aggr shape 0 (default wrap-first policy)
│ or remain raw with --direct-joins
▼
ix_aggr proofs carrying uniform CheckEnv statements
│
├── flat ix_aggr shapes 2–5 below the configured subject threshold
│ subjects := canonical(subjectsL ∪ subjectsR)
│ assumptions := (assumptionsL ∪ assumptionsR) ∖ subjects
│
└── structural ix_aggr shapes 6–9 above the threshold
subjects := nodeHash(subjectRootL, subjectRootR)
assumptions := candidates carried or discharged by inclusion path
▼
Ixon.Proof {
claim := CheckEnv(manifest-relative subject root, none),
proof := one recursive Multi-STARK root proof
}

A manifest with one retained (nonempty) shard is also a valid deliverable: its
root is one shape-0 wrapper, with no self-join padding (including under
--direct-joins). Zero-constant manifest leaves are pruned after raw coverage
validation:

raw manifest
|
| validate exact environment coverage before pruning
v
drop zero-constant leaves + contract unary tree nodes + remap retained ids
|
+-- one retained leaf --> wrap once and persist the ix_aggr root
|
`-- multiple leaves --> fold flat/structural joins as above

This is the stage-2 aggregation layer from
plans/aggregate-first-pipeline.md: shard proofs are aggregated before any
terminal KZG/SP1 compression, so a later wrapper only needs to consume one root
proof. Terminal compression remains outside this PR.

Section 14 implementation is complete through M1-f in this branch. The
ap/ix-aggr single-entrypoint base is reconciled with WP-E2's compact stored
proof / expanded recursive-advice boundary, and its one ix_aggr entrypoint
supports wraps, flat pairs, and structural pairs across both IxVM and recursive
children. ix aggregate now uses that backend for planning, proving, cache
resume, and final verification; ix verify --aggregate derives one uniform
outer claim and performs value-based environment/manifest checks. The old
ix aggr command and all three-entrypoint production wiring are retired. The
older Multi-STARK aggregate implementation remains as an Ix-agnostic regression
surface, not as a CLI backend. Production semantics, activation coverage, and
benchmarking now live entirely against ix_aggr. M1-f lands the pinned
four-shard bench-aggregate-policy handoff and ports the stable typecheck join
metric to direct shape 2; only execution of the policy comparison on the large
box remains. Operationally this is one future handoff: Arthur will check out
b70042d6d1a029bb8e1d541da7aef5e0613d67e6 once, run the inherited M1-e
correctness suite as untimed preflight, and then benchmark both policies. There
is no separate M1-e box run.

Measured performance (2026-08-29)

Arthur Paulino ran the pre-convergence pipeline end-to-end on four real Init
shard proofs (shards 8–11 of a 16-way partition, 12,493 constants; base shard
proving excluded; native verification of each output included; ~512 GB box).
These are the standing M1-f comparison baseline; the converged wrap-first and
direct policies have not yet been rerun on that box. The recursion used the
conservative q=100 / PoW 20 defaults this PR keeps:

StageTimePeak RAMOutput proof
Lift × 498.8–103.0 s (mean ≈ 101 s)186.8–195.8 GiB8,162,462 B each
Join 8+9, 10+1148.9 / 48.5 s~102.5 GiB9,502,843 B
Root join85.8 s156.9 GiB9,455,498 B
Total (serialized)10:23 wall195.8 GiB9,455,498 B

Notes: lift cost is shard-content-independent (four lifts within ±2%, tracking
the ~constant 20–23 MB shard-proof shape); the measured lift peak brackets the
scheduler's 195 GiB weight placeholder exactly; and an unserialized run of all
four lifts OOM'd at ~492 GiB — precisely the behavior the WP-B admission gate
prevents (on a 512 GB box its weights admit two lifts). Because shards 8–11
are a strict partition subset, the root carries a real frontier assumption
set, so the joins exercised genuine discharge work. Full analysis, including
the comparison against direct (unlifted) IxVM joins, is in
plans/aggregate-first-pipeline.md §3.4 and the Zulip thread.

What lands

Protocol / circuit layer:

  • One production recursion entrypoint, ix_aggr, under one verifying key.
    Shapes 0–1 wrap one IxVM/recursive child, shapes 2–5 fold flat pairs, and
    shapes 6–9 fold structural pairs for every child-kind combination.
  • Recursive validation of wrap, flat-pair, and structural-pair children with
    transitive verifying-key, function-index, and allowed-system binding. A
    shape hint selects the verified form but cannot weaken its checks.
  • Strict in-circuit CheckEnv decoding and canonical flat set folding.
  • Structural subject folding with per-candidate Merkle discharge paths while
    keeping output assumption sets canonical and deduplicated.
  • One 80-byte protocol identity containing the IxVM/self verifying-key
    digests and their two accepted entrypoint indices; every recursive node has
    the same 18-word outer-claim layout regardless of shape.
  • Structural outputs bind nodeHash(leftRoot, rightRoot) and account for each
    unique assumption candidate through a strict Merkle-path-or-carry choice.
  • Host helpers construct structural roots, survivors, and path advice. Channel
    6 carries both the one-byte shape at key [0] and candidate path payloads at
    raw 32-byte address keys; the key shapes are disjoint.
  • Lean/Rust execute and prove FFI accept a compact keyed path blob, whose
    framing is decoded strictly before the circuit independently validates
    payload semantics.
  • The generated ix_aggr executor is regenerated at 2,061,361 bytes / 248
    Aiur functions and parity-tested against the interpreter.

Host driver and operations:

  • Strict .ixes aggregation-tree parsing and post-order lowering, with a
    balanced fallback for legacy manifests; validate-then-prune removal of
    zero-constant leaves with unary tree contraction (and a Rust writer that
    stops emitting them).
  • Single-shard roots: a one-retained-shard manifest always produces one
    shape-0 ix_aggr wrapper, including under --direct-joins.
  • A monotone leaf-count scheduler controlled by --structural-above, with a
    default wrap-first plan (shapes 0 then 5/9) and an explicit
    --direct-joins plan that derives heterogeneous shapes from actual child
    kinds.
  • Versioned, content-addressed aggregate cache and resume (WP-A): version 2
    binds the uniform outer claim, and reuse occurs only after content-digest,
    claim, decode, and native verification all pass; --no-cache bypasses.
  • Parallel execution of ready wraps/pairs as a dependency DAG under --jobs
    and --max-ram (default 92% of MemTotal), heaviest-first, with failure
    draining (WP-B). Admission weights are shape-aware: 4 GiB for a retained
    raw IxVM leaf, 195 GiB for wraps/self pairs, 390 GiB for IxVM/IxVM pairs,
    and 340 GiB for mixed pairs.
  • A recursion-parameter split (WP-E1): aggregate proving/verification share an
    explicit RecursionParameters value, defaulting to today's q=100 values;
    the q=50+PoW policy decision is deliberately deferred.
  • A deterministic 132-case production activation audit across shapes 0–9:
    two controlled child trace heights, both wraps, every flat/structural
    child-kind pair, optional assumptions per side, and discharge/carry. The
    complete matrix runs twice with a stable signature and observes every
    catalogued circuit; dummy-call padding remains intentionally deferred.
  • Claim-only shard preparation (no discarded dependency-byte closure walks)
    with a one-pass multi-root witness closure for proving.
  • ix aggregate --ixe E --ixes M [--structural-above N] [--direct-joins] [--jobs N] [--max-ram G] [--no-cache] [--plan-only] <proof>... and
    single-entrypoint aggregate verification with cryptographic-only,
    environment-bound, and manifest-bound value checks. The duplicate
    ix aggr command is removed.
  • Opt-in aggregate benchmarks: a tiny two-shard Init pair end-to-end bench and
    a flat-join bench (singleton CheckEnv shards → two lifts → verified flat
    join), with join metrics wired into reporting/dashboards, the pre-E2 lift
    proof size pinned at 7,986,166 bytes, and the post-E2 delta measured.
  • Positive, transitive-recursion, scheduler, cache, pruning, parity,
    malformed-input, and semantic-tampering tests: the aggregate-first suite
    has 58 cases, plus the separate activation matrix.

Protocol design

One recursion system, one entrypoint

Production builds Aggr.ixAggr, the shared Multi-STARK verifier closure pruned
to one ix_aggr entrypoint. A one-byte advice hint selects the exact verified
shape:

ShapeChildrenFold
0IxVMpass-through wrap
1ix_aggrpass-through wrap
2–5IxVM/ix_aggr pair (2 + 2·left + right)canonical flat fold
6–9IxVM/ix_aggr pair (6 + 2·left + right)structural fold

The hint is advice, not authority: each arm verifies the child proof(s) under
the key required by the encoded child kinds, then requires that kind's exact
claim layout. A wrong shape therefore fails proof verification, function-index
binding, or claim decoding.

Uniform public statement and pinned protocol identity

Every wrap and pair exposes the same public-input shape:

ix_aggr(allowed_digest: [G; 8], out_claim_digest: [G; 8])

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:

  1. the 80-byte allowed-system blob; and
  2. the serialized output CheckEnv claim.

The allowed blob is:

blake3(ixvm_vk)
|| verify_claim_idx:u64le
|| blake3(ix_aggr_vk)
|| ix_aggr_idx:u64le

The circuit requires exactly 80 bytes. 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
another function in those systems; binding the two accepted 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.
Every persisted aggregate outer claim is consequently the same 18-word form:
the ix_aggr function selector followed by the packed identity and output
claim digests. Shapes are witness choices and do not become root-kind metadata.

Child-proof validation and transitive pinning

Each pair reads the verifying key required by each child kind, checks its
Blake3 digest against the allowed blob, and deserializes it strictly. Wraps do
the same for their one child. For every child the selected arm:

  1. reads and fully consumes one proof and exactly one outer claim;
  2. runs the Multi-STARK structural verifier;
  3. runs ood_verify, including Fiat-Shamir replay, lookup accumulators,
    quotient/OOD checks, Merkle openings, and FRI verification; and
  4. decodes the statement according to the constrained child kind and function
    index.

The accepted forms are:

  • IxVM child: its system digest must equal blake3(ixvm_vk). Its claims
    digest opens to exactly one 10-word IxVM claim at the pinned
    verify_claim_idx, whose digest opens to a strict serialized CheckEnv
    claim.
  • ix_aggr child: its 18-word claim must use the pinned ix_aggr_idx; its
    first public digest must equal the current allowed_digest, transitively
    pinning both systems and both entrypoints; and its second digest opens to its
    output CheckEnv claim.

Any other index, shape, digest, trailing byte, or failed proof check rejects.
This keeps wrap→pair, flat→pair, and structural→structural composition on one
statement format.

Flat canonical folding

For child statements (S_L, A_L) and (S_R, A_R), flat shapes 2–5 enforce:

S_out = S_L ∪ S_R
A_out = (A_L ∪ A_R) ∖ S_out

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 u32 words using the
full-domain u32_less_than primitive. The implementation never uses Aiur
pointer 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

Structural shapes 6–9 perform the same allowed-blob binding, child-key
binding, two child-proof verifications, child decoding, and output-claim
binding as the flat shapes. They replace all subject-tree loading and set
re-rooting with:

S_out.root = nodeHash(S_L.root, S_R.root)

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:

  • carried (0): no bytes may follow, and the candidate must be the next
    value in the output assumption list; or
  • discharged (1): a bounded Merkle path from
    leafHash(candidate) to S_out.root must verify.

Path payloads are strict:

0
1 || count:u8 || repeated(side:u8 || sibling[32])

count is limited to 64. side = 0 hashes nodeHash(sibling, current) and
side = 1 hashes nodeHash(current, sibling); any other side rejects. The
payload 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

All ix_aggr shapes use the same seven-channel IO layout. Digest/root bindings
are checked before the corresponding bytes are decoded.

ChannelKeyValue
0[0], [1]first and optional second child proof advice bytes
1[kind]verifying-key bytes (0 = IxVM, 1 = ix_aggr)
2[0], [1], [2]left/right outer claims and output CheckEnv claim
3[0]80-byte allowed-system blob
4packed Blake3 digestnested claim preimages
5raw 32-byte addressserialized canonical trees keyed by root
6[0] / raw candidate addressshape byte / carried-or-discharged path choice

Lean passes channels 4–6 across FFI as compact framed blobs:

count:u32le || repeated(key[32] || payload_len:u32le || payload)

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.

Wraps receive no tree or path advice. Flat pairs receive full
subject/assumption tree advice and an empty path blob. Structural pairs receive
only the input/output assumption trees plus one path choice for every
deduplicated candidate; subject trees are never opened.

Manifest and host pipeline

.ixes aggregation tree

parseIxesManifest exposes both shard block lists and the optional binary
aggregation tree stored at the manifest tail. It:

  • requires nonzero, contiguous shard IDs;
  • validates option and tree tags;
  • requires tree leaves to be exactly the manifest shard set;
  • rejects duplicate/out-of-range leaves and trailing bytes; and
  • synthesizes a balanced ascending-ID tree for legacy manifests without a
    tree tail or with an explicit absent-tree tag.

The tree lowers to post-order FoldOp slots. Every join refers only to earlier
slots and the last slot is the root, providing a direct serial schedule and the
basis for cache keys and parallel execution.

Validate first, then prune empty manifest leaves

IxesManifestView retains the original shard id for every dense shard slot.
pruneEmpty:

  1. counts the environment constants owned by every raw shard;
  2. drops only shards with a count of zero;
  3. contracts aggregation-tree nodes with a removed child;
  4. densely remaps retained leaf indices; and
  5. returns the retained per-shard subject counts with the pruned view.

The aggregate and manifest-aware verifier paths deliberately call
shardsCover on the raw manifest before this transformation. Coverage
still establishes that every environment constant is owned exactly once. The
subsequent zero count is therefore evidence that a removed leaf contributes no
subject; pruning is not allowed to hide a missing or duplicated constant.

Original shard ids are preserved separately from dense scheduling ids, so CLI
plans and proof-binding errors continue to identify the source manifest shard.
An all-empty manifest is rejected because it cannot produce a root.

For a tree node node(left, right), pruning follows:

both retained -> node(left', right')
only left retained -> left'
only right retained -> right'
neither retained -> removed

The Rust .ixes writer (crates/kernel/src/shard.rs) also normalizes new
manifests before serialization — block-empty shard records are omitted, ids
rewritten densely, the tree pruned/contracted/remapped, and cross-ingress
recomputed. The Lean-side pass remains necessary for legacy manifests and for
the stronger environment-relative condition (a shard may contain blocks yet
own zero constants).

Monotone structural scheduling

The host counts actual owned constants for each shard after the disjoint-cover
check. schedulePlan annotates every slot with its cumulative subject count and
chooses a structural pair exactly when:

left.subjectCount + right.subjectCount > structuralAbove

The default threshold is 4096; --structural-above 0 makes every join
structural 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.

By default, every leaf is an ix_aggr shape-0 wrap, so binary slots are shape
5 below the threshold and shape 9 above it. With --direct-joins, a
multi-shard plan keeps leaves as raw IxVM children and derives each pair shape
from its actual child kinds: 2–5 for flat pairs or 6–9 for structural pairs.
The monotone threshold rule is unchanged.

The proving loop checks reconstructed subject counts against the schedule
before starting expensive work.

Singleton roots

ix aggregate imposes no two-shard minimum. The fold plan for one retained
leaf contains one shape-0 wrap and zero pairs; that wrapper and its CheckEnv
claim are persisted as the aggregate root. Even --direct-joins wraps a
singleton because the deliverable must verify under the one production
entrypoint. A self-pair would add work and manufacture a different statement.
Consequences:

  • exactly one proof is required per nonempty shard;
  • --plan-only reports one wrap and zero binary pairs for a singleton;
  • multi-shard flat/structural scheduling is unchanged; and
  • the final unconditional-root check always applies to an ix_aggr root.

ix aggregate

ix aggregate --ixe ENV.ixe --ixes SHARDS.ixes \ [--structural-above N] [--direct-joins] [--jobs N] [--max-ram G] [--no-cache] \ SHARD_PROOF_ADDR...

The command:

  1. deserializes the environment and manifest and verifies a disjoint exact
    shard cover on the raw manifest, then prunes zero-constant leaves;
  2. reconstructs every retained shard's exact CheckEnv claim and canonical
    trees (claim-only preparation — no dependency-byte closure);
  3. matches proof arguments to shards in any order by bundled claim digest,
    then requires exact claim equality and one proof per nonempty shard;
  4. natively verifies every IxVM shard proof before expensive recursion work;
  5. builds the IxVM and ix_aggr recursion systems and the pinned 80-byte
    allowed blob;
  6. derives every slot's statement, child kind, shape, uniform outer claim, and
    version-2 cache key up front;
  7. executes ready wraps and flat/structural pairs as a RAM-gated dependency
    DAG, resuming verified cached slots and persisting each completed wrapper
    (raw direct-policy leaves are verified inputs, not cache slots);
  8. canonicalizes all leaves under the final subject tree and checks that they
    reproduce the environment's canonical root;
  9. requires no remaining assumptions and natively verifies the outer proof at
    the one ix_aggr root entrypoint; and
  10. persists an Ixon.Proof containing the output CheckEnv claim and proof.

--plan-only stops after coverage validation and scheduling, resolving and
printing the scheduler policy without loading proofs or compiling recursion
systems:

ix aggregate --ixe ENV.ixe --ixes SHARDS.ixes \ --structural-above 0 --plan-only

ix verify --aggregate

Aggregate roots reuse Ixon.Proof, so verification is selected explicitly.
The verifier never accepts root-kind metadata. It serializes the bundled
CheckEnv value and reconstructs the only valid outer claim:

claim_bytes := serialize(CheckEnv(...))
outer_claim := ix_aggr(
blake3(allowed_blob),
blake3(claim_bytes))
InvocationCryptographic entrypointExpected statement value
ix verify --aggregate <proof>ix_aggrbundled claim; cryptographic proof only
ix verify --aggregate --ixe E <proof>ix_aggrcanonical environment root, unconditional
ix verify --aggregate --ixe E --ixes M <proof>ix_aggrpruned manifest fold, unconditional

Environment-only mode compares the bundled value to the canonical environment
root; a manifest-relative structural value therefore rejects naturally, with
no root-kind branch. Manifest-aware verification repeats pruning and the fold
schedule (the threshold must match the proving run), reconstructs every leaf
statement, folds the exact hybrid value, requires no assumptions, and verifies
the same ix_aggr outer claim. Existing non-aggregate proof and
shard-composition verification paths are unchanged.

Claim-only shard preparation

shardCheckEnvClaimTrees constructs only the CheckEnv claim and its
canonical subject/assumption trees. Aggregate planning, shard-digest
reconstruction, and aggregate verification use this path because none of them
consumes dependency bytes. shardCheckEnvClaim remains the witness-building
entry point; it reuses the same claim/tree result and computes the byte
closure once from the union of the owned and available primitive roots.
Reachability distributes over that root union, so this preserves the old
closure while avoiding repeated traversal of shared subgraphs.

Recursion parameters (WP-E1)

Aggregation and aggregate verification consume one shared
MultiStark.RecursionParameters value containing the recursion commitment and
FRI configurations, carried through runAggregateCmdWith/runVerifyCmdWith
into the shared MultiStark.buildRecursionSystem. The CLI entrypoints pass
defaultRecursionParameters, whose fields are exactly today's canonical Aiur
defaults. This is a mechanism split, not a policy change:

  • ordinary IxVM proving and verification remain on the canonical q=100
    configuration;
  • recursion proofs also remain q=100 with the same commitment and PoW values;
  • no new CLI flag selects security parameters; and
  • choosing q=50 plus a compensating PoW policy remains the explicit §9.5
    follow-up decision.

The recursion verifying key serializes and binds both parameter sets, and its
digest is included in every aggregate node's allowed-system blob, so any parameter
change changes the recursive protocol identity without a public-input or
allowed-blob format change. RecursionParameters.cacheFriBytes pins the
40-byte fri_params_ser cache-key component:

logFinalPolyLen:u64-le || maxLogArity:u64-le || numQueries:u64-le ||
commitProofOfWorkBits:u64-le || queryProofOfWorkBits:u64-le

Commitment parameters need no second cache component: changing them changes
the recursion-vk digest already present in the key.

Pruned FRI multiproofs (WP-E2)

The four-commit origin/update-multi-stark series is transplanted onto this
stack: Rust 1.98, the 249b7405 multi-stark revision on P3 v0.6, the matching
Blake3/Lean-FFI pins, and recursive proof-advice encoding. The new proof wire
format deduplicates shared Merkle siblings across sampled FRI queries. Width
and trace-height binding and the vk's checked max_multiplicity travel with
the dependency update; no aggregate claim or public-input format changes.

There are now deliberately two proof encodings. Proof.toBytes is the compact
wire form used by wrappers, native verification, the store, and WP-A cache
entries. Recursive execution still expects one path per query, so callers use
AiurSystem.proofToAdviceBytes immediately before a wrap or pair. The CLI,
pair/typecheck benchmarks, semantic tests, and activation audit all enforce
that boundary. Cached proofs stay compact at rest and are expanded only when
consumed as a parent pair's child.

The dependency's compact format is not backward-decodable. Store-facing CLI
verification therefore uses Proof.ofBytesChecked and reports a normal error
for legacy or corrupt proofs instead of crossing the unchecked decoder and
aborting. P3 v0.6 also requires a positive FRI query count; benchmark CLIs
reject --queries 0, and the old q=0 join smoke gate is replaced by q=1.
The Rust 1.98 migration also applies its new strict-Clippy equivalents for
fixed-size digest chunking and fallible metadata-size lookup; these are
mechanical, semantics-preserving rewrites.

Verified cache and resume (WP-A)

Before the first proof, the driver derives every fold slot's CheckEnv
statement, exact outer Aiur claim, and cache key, using the same
flat/structural fold policy as proving — so a manifest rebudget or statement
change invalidates only the changed subtree. The key:

blake3(
aggregateCacheVersion:u64-le ||
blake3(recursion_vk) ||
recursion_fri_params:40-bytes ||
serializeClaims([outer_claim])
)

The uniform outer claim already includes the ix_aggr entrypoint index and
public input, hence the allowed-system blob, output CheckEnv, both
verifying-key identities, and both accepted entrypoint indices. Each completed
recursive slot is stored as an Ixon.Proof wrapper in the content-addressed
~/.ix/store; a raw --direct-joins leaf is only an input and is not cached.
The wipeable index ~/.ix/cache/aggregate/<key> holds only the wrapper's store
address and is updated via temp-file + atomic rename. M1-d bumps
aggregateCacheVersion from 1 to 2, so old outer-claim entries miss cleanly.

A hit is an untrusted hint, not authority. Reuse requires all of: a
well-formed index address; store bytes whose Blake3 digest equals it; a
decodable wrapper; a bundled claim exactly equal to the precomputed slot
CheckEnv; a safely decoded Aiur proof (Aiur.Proof.ofBytesChecked — the
former trusted-byte constructor panicked on truncated bincode, and malformed
cache bytes must never abort the command); and native verification under the
exact precomputed outer claim and local recursion system. Any failure logs a
miss and re-proves. Cache write failures are warnings. On a valid hit the
host reconstructs the slot's preimages from the manifest statement, so
parents need no trusted metadata from the cache. --no-cache bypasses index
reads and intermediate writes while still persisting the final root wrapper.

RAM-gated parallel scheduling (WP-B)

The post-order fold executes as a dependency DAG: leaves are ready
immediately; a join becomes ready when both child slots have completed. The
controller sorts ready work by descending RAM weight (slot number breaks
ties) and starts one dedicated IO.asTask per admitted slot around the
existing proof FFI. Two independent limits govern admission:

  • --jobs N caps active slots; absent or 0 means every ready slot may run
    if it fits the budget;
  • --max-ram G caps total reserved GiB in flight, defaulting to 92% of Linux
    MemTotal.

As in the existing Rust RamGate, a slot whose estimate exceeds the entire
budget is admitted only while nothing else runs — a conservative estimate
serializes work rather than deadlocking. The calibration-pending per-shape
weights are:

raw IxVM leaf = 4 GiB
shape 0/1 wrap = 195 GiB
shape 2/6 IxVM + IxVM = 390 GiB
shape 3/4/7/8 mixed pair = 340 GiB
shape 5 self + self flat = 195 GiB + 1 MiB × subject leaves
shape 9 self + self struct = 195 GiB

The 195 GiB wrap reserve carries forward the recorded q=100 lift upper bound —
the measured peaks above (186.8–195.8 GiB) bracket it exactly. The 390/340 GiB
direct and mixed estimates deliberately keep the non-default policy out of the
256 GiB fleet tier pending M1-f calibration; measured self-pair peaks
(102.5–156.9 GiB) make their weights conservative in the safe direction. Real
calibration on a known-core box will tighten them without changing scheduler
semantics.

Results are installed by slot index, never arrival order; each worker gets an
immutable snapshot of its completed children, so parallel timing cannot
change parent advice. On failure the controller stops admitting, drains every
running proof, and reports the lowest failed slot; successful independent
tasks may finish publishing valid cache entries. Concurrent proofs share
Aiur's rayon global pool, so throughput can sub-scale even when memory
permits multiple slots; process-per-slot execution is a measurement-driven
follow-up.

Converged activation audit (WP-D + M1-e)

lake exe IxTests aggregate-activation is a dedicated diagnostic kept out of
the default suite. It executes the generated production ix_aggr path across
4 wraps and 128 pairs: two controlled child trace heights, shapes 0–9,
optional assumptions independently per side, complete discharge versus
explicit carry, and all four child-kind combinations in both flat and
structural modes. Every execution returns per-circuit (uniqueRows, totalHits); the runner performs the full 132-case matrix twice and rejects any
difference before rendering a deterministic per-shape Markdown report.
Current signature:

263a3087d42340613b012d50a9416e23549f85aa1ea425e86bf0a216c7b3e27d

Result: 183 audited circuits — 154 active in every case, 29 input-dependent,
zero never observed. The variable set concentrates in the expected paths:
wrap dispatch, canonical set folding, structural Merkle discharge, and
child-kind decoding. Fixed-height Bytes1/Bytes2 circuits are
outside the execute FFI's query-count array and are called out explicitly.
The audit records information only; dummy calls would add cost for the
currently planned SP1 terminal and should be chosen only if a future static
terminal requires input-independent activation.

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.executeMultiStarkJoin for generated or interpreted flat
    and structural join execution;
  • AiurSystem.proveMultiStarkJoin for witness generation and proving;
  • JoinAdvice, JoinPreimage, JoinTree, JoinPath, strict framed decoders,
    and the seven-channel join_io_buffer;
  • the matching rs_aiur_multi_stark_join_execute and
    rs_aiur_multi_stark_join_prove externs; and
  • Aiur.Proof.ofBytesChecked for panic-free store-boundary proof decoding.

The retained three-entrypoint executor is parity-checked against the bytecode
interpreter on both output and every circuit's unique-row/total-hit counts. The
combined verifier contains 247 Aiur functions; the regenerated Rust file is
2,239,894 bytes (ixvm: 5,807,375 bytes / 780 functions). Most line churn is
generated code. The protocol source of truth is
Ix/MultiStark/Aggregate.lean; the native advice contract lives in
aiur_multi_stark_runner.rs. It is now an Ix-agnostic regression/reference backend,
not the production CLI backend.

The production ix_aggr executor is independently parity-checked for wrap,
flat, and structural shapes. It contains 248 Aiur functions and its generated
Rust file is 2,061,361 bytes. Its source of truth is
Ix/Aggr/Circuit.lean, with native advice construction in
aiur_ix_aggr_runner.rs; all three generated artifacts are checked with
ix codegen --check.

Benchmarks

M1-f replaces the legacy two-shard/three-entrypoint runner with
bench-aggregate-policy, a production ix_aggr benchmark for one exact
four-shard fixture. It selects the four retained leaves from a validated and
pruned manifest, contracts the surrounding tree without changing orientation,
and independently reconstructs the expected host root. Wrap-first uses shape 0
leaves; direct mode keeps IxVM leaves raw. Both policies then select their flat
or structural heterogeneous shapes from the production plan. Measured runs
require q=100, jobs=1, --no-cache, and JSON output; the harness natively
verifies all four inputs and every recursive output, persists every completed
slot as an ordinary Ixon.Proof, and writes resumable per-slot and whole-run
timing/RSS/proof metadata.

The existing bench-typecheck --recursive --join row keeps its stable
join-* reporting/dashboard schema, but its join phase now exercises direct
ix_aggr shape 2 instead of the retired join_two circuit. Historical size
baselines remain recorded: 7,986,166 bytes (7,986,204 with the Ixon.Proof
wrapper) for a one-constant q=100 lift under the pre-E2 247-function system,
versus 7,443,023 bytes (7,443,061-byte wrapper) after WP-E2, a 543,143-byte /
6.80% reduction. Pruned multiproof length depends on sampled query-path
overlap, so a positive-PoW output is now a sample rather than an exact
byte-length pin. The deterministic q=100/PoW-0 regression signal is 7,447,279
bytes and 38,850,942,825 recursive-verifier FFT cost. The target-box policy
comparison is the remaining measurement, not remaining harness work.

Test coverage

The legacy aggregate-first suite uses a small stand-in child system so it can
create real Multi-STARK child proofs without the tens to hundreds of GiB
required by a full recursion proof. Those proofs are consumed by the retained
join circuits, so proof validation, statement decoding, set/path folding,
native advice, and transitive recursion are exercised end-to-end at execution
time.

The 57 passing legacy aggregate-first cases cover:

  • canonical flat and structural host folds; manifest parsing, validation, and
    post-order lowering;
  • distinct lift/flat/structural compiled entrypoint identities;
  • honest flat union and cross-child discharge; structural path discharge plus
    a carried assumption;
  • flat-join and structural-join outer claim layout and native verification;
  • transitively pinned flat and structural children; rejection of a child
    carrying a different allowed digest;
  • generated/interpreter parity for both join modes; threshold scheduling that
    is flat below and structural above monotonically;
  • strict keyed-blob framing; wrong-root and tampered-sibling paths; a missing
    path choice; a carried candidate omitted from the output; the obsolete
    88-byte allowed blob; a flat join fed a genuinely free-form structural
    child root; omitted/extra assumptions or subjects; noncanonical unsorted
    trees; tampered child proofs;
  • a synthetic three-leaf manifest with empty outer leaves: raw coverage before
    pruning, contraction, dense remapping, and singleton-lift classification;
  • claim-only shard preparation preserving exact subject/assumption trees,
    with the one-pass witness closure matching the legacy per-owned-root union;
  • exact legacy verifier reconstruction of a lift outer claim and native
    verification under it;
  • byte-for-byte equality of the shared default recursion builder with the
    former direct construction; the exact 40-byte FRI cache encoding;
    independent recursion-vk identity changes for FRI and commitment overrides;
  • cache-key invalidation across version, recursion vk, FRI parameters, and
    outer claim; whole-plan statement/claim/key derivation before proving;
    atomic index creation, corrupt-index detection and repair; a
    content-addressed, claim-bound, natively verified resume hit; mismatched
    claim rejection; corrupt store-content fallback; checked rejection of
    malformed proof bytes without a Rust panic;
  • heaviest-first admission with deterministic tie-breaking; job and byte
    ceilings; dependency release; admit-alone oversize handling; the flat-join
    affine weight and MemTotal parser; jobs=1/jobs=2 payload equality;
    failure draining without admitting a dependent join; jobs=1/jobs=2
    byte-identical recursive wrappers under canonical zero-PoW grinding, with
    each scheduled child decoded and verified; and
  • Rust writer normalization plus round-tripping a genuinely noncanonical
    structural CheckEnv root.

The wrapper-byte scheduler gate uses zero PoW deliberately: with the pinned
positive-PoW implementation, rayon's find_any selects any passing grind
witness, so two valid serial proofs already differ in bytes. Production
jobs=1/jobs=2 correctness is the stronger protocol invariant — exact claims
and native verification under the same recursion system. The primitive
Multi-STARK and recursive-verifier suites were rerun to guard the shared
verifier and lift paths, and the separate activation gate covers 132 accepted
shape/input cases twice.

The focused ix-aggr command now runs 93 passing checks. Its 42
circuit/shape/plan checks cover both wraps, all flat and structural child-kind
pairs, a nested structural self child, generated/interpreter parity, strict
tree and native keyed-blob framing, identity/shape closure, wrong or tampered
paths, omitted carries, unsorted trees, flat↔structural hint confusion, a flat
parent fed a free-form structural root, and exact four-shard wrap-first/direct
plans. Its 51 converged semantic checks cover uniform claims, cache v2 key
invalidation and verified resume, recursion parameters, manifest
validation/pruning, singleton/canonical/hybrid value reconstruction,
shard-preparation closure sharing, wrap-first/direct planning, per-shape RAM
admission, dependency/failure behavior, and jobs=1/jobs=2 determinism including
concurrent zero-PoW proving. Rust unit tests separately pin channel-6 key layout
and strict path-blob framing.

Validation run

All of the following pass on the current stack:

nix develop --command lake exe IxTests aggregate-first # 57/57nix develop --command lake exe IxTests ix-aggr # 93/93nix develop --command lake exe IxTests aggregate-activation # 132 cases × 2 stable passesnix develop --command lake exe IxTests multi-stark # 12/12nix develop --command lake exe IxTests recursive-verifier # 6/6nix develop --command lake build IxTests bench-typecheck bench-recursion-debug bench-aggregate-policy ixnix develop --command cargo test -p ixvm-codegen aiur_multi_stark_runnernix develop --command cargo test -p ixvm-codegen aiur_ix_aggr_runnernix develop --command cargo test -p ix-kernel shard::tests # 23/23nix develop --command cargo test -p ixon test_check_env_claim_accepts_structural_rootnix develop --command cargo clippy -p ixvm-codegen -p ix-ffi -p ix-kernel -p ixon --tests -- -D warningsnix develop --command cargo fmt --all -- --checknix develop --command lake exe ix codegen --checkgit diff --check

Both benchmark entrypoints also reject --queries 0 with a usage error before
building or proving, and a stored pre-E2 proof now reports a checked decode
error rather than aborting. The fresh post-E2 aggregate fixture verifies
against both its .ixe and .ixes inputs.

A q=1 live smoke of the converged typecheck join also executed, proved, and
verified direct shape 2 successfully: 1.221742 s execution, 26,738,665,748 FFT
cost, 52.380108 s proving, 21,565,714,432-byte peak RSS, 401,340-byte proof,
and 0.002616 s verification. The M1-f policy harness passed plan-only smoke for
both policies and emitted the expected resumable JSON schema.

Pre-convergence real-environment gates on the 124 GB development host (kept
as WP-A/WP-B baselines for the M1-f rerun):

  • A two-shard manifest from the 5,986-constant tc-parity.ixe environment
    passes coverage and structural scheduling with threshold zero
    (2 lifts + 1 binary joins (1 structural)), and its one-shard variant
    plans 1 lifts + 0 binary joins.
  • Before E2, a one-constant q=100 production lift ran end-to-end through the
    cache and scheduler, establishing WP-A's invalidation/repair baseline:
PassResultWall
Initialproved and cached root 090bea6f…ca1535~64 s
Identical rerunverified cache hit, same root1.66 s
Truncated indexclean miss, re-proof, atomic repair, same root~38 s
Post-repair rerunverified cache hit, same root1.79 s
--jobs 2 --max-ram 400195 GiB reserved, verified hit, same root1.45 s

At scale, the measured pre-E2 4-shard Init run in Measured performance
above exercised the former backend with real shard proofs, four q=100 lifts,
two lower joins, and a root join with genuine assumption discharge. The pinned
M1-f harness will repeat this fixture under both converged policies.

Soundness summary

  • Every present child is fully verified under the verifying key selected by
    its shape-constrained kind.
  • Child function indices are constrained to IxVM verify_claim or ix_aggr,
    and every recursive child must carry the same allowed digest, pinning the
    two verifying keys and two entrypoints transitively.
  • Claims and allowed data are Blake3-bound and strictly parsed with no trailing
    bytes.
  • Flat mode reopens canonical roots and proves exact union/difference;
    structural mode proves the exact root-of-roots relation and accounts for
    every unique input assumption through either inclusion or explicit carry.
  • A bogus path, dropped candidate, extra survivor, malformed choice, or
    noncanonical assumption tree rejects. Over-carrying and duplicate subject
    leaves can only weaken the output claim; the CLI requires an unconditional
    final root.
  • Coverage is checked on the unmodified manifest, so pruning cannot conceal a
    hole or overlap; only leaves with an environment-derived owned-constant
    count of zero are removed, and contraction preserves the order and shape of
    retained leaves.
  • A singleton root is a verified shape-0 wrapper, never a raw-proof shortcut;
    the circuit binds its output digest directly to the pinned IxVM child's
    CheckEnv digest.
  • Manifest-aware verification independently repeats pruning, scheduling, and
    value folding, then verifies the one derived ix_aggr outer claim. No
    claimed proof kind or shape is accepted from wrapper metadata.
  • Proving and verification build the recursion system from the same explicit
    parameter object; any parameter change necessarily changes the recursion vk,
    allowed-system identity, and cache key.
  • Cache keys bind version, recursion-vk digest, explicit FRI bytes, and exact
    outer claim, all derived before proving; hits re-hash store content, bind
    the claim, decode without panicking, and verify natively. Corrupt, missing,
    or stale entries are misses, never trusted successes or command failures.
  • A pair is admitted only after both child slots complete; immutable
    snapshots and indexed installation make arrival order irrelevant. The
    controller owns all reservations; failures drain running work before exit.
  • Free-form structural roots are serialized as opaque commitments
    (crates/kernel/src/claim.rs); canonical openings are required only by
    protocol paths that explicitly need them.

Review map

Commit-by-commit review is recommended — each commit is feature-scoped with
its own tests (map in the first comment / commit list).

AreaPrimary filesWhat to review
Legacy regression protocolIx/MultiStark/Aggregate.lean, Ix/MultiStark.leanpre-convergence 96-byte identity and semantic/audit reference surface
Production circuitIx/Aggr/Circuit.lean, Ix/Aggr.lean80-byte identity, uniform claims, shapes 0–9, heterogeneous child pinning, structural path checks
Converged host modelIx/Aggr/Host.leanfree-form structural roots, canonical survivors, path extraction
Host statement modelIx/MultiStark/Host.leanfree-form subject roots, canonical assumptions, path extraction
Manifest parsing + pruningIx/Cli/CheckCmd.leanstrict tree parsing, validate-before-prune, unary contraction, dense remap
Aggregate driverIx/Cli/AggregateCmd.leanwrap-first/direct shape policy, singleton wrap, uniform slot claims, cache v2, per-shape RAM weights, verified DAG resume/failure draining
Root verificationIx/Cli/VerifyCmd.leanone ix_aggr outer claim, value-based environment comparison, manifest-relative hybrid reconstruction
Recursion parametersIx/MultiStark.lean, Ix/Aggr.lean, Ix/Cli/{Aggregate,Verify}Cmd.leanshared config plumbing, deterministic systems, stable FRI cache encoding
Pruned FRI multiproofsIx/Aiur/{Protocol,Semantics/BytecodeFfi}.lean, Ix/Cli/{Aggregate,Verify}Cmd.lean, crates/ffi/src/aiur/protocol.rscompact persisted proof vs expanded recursive advice boundary, checked legacy-proof failure
Cache/store boundaryIx/Store.lean, Ix/Aiur/Protocol.lean, crates/ffi/src/aiur/protocol.rswipeable namespace, checked proof decoding, corrupt-data fallthrough
Shard preparationIx/IxVM/ClaimHarness.lean, Ix/Cli/{Aggregate,Check,Verify}Cmd.leanclaim/tree-only consumers, one-pass multi-root witness closure
Native advicecrates/ixvm-codegen/src/aiur_multi_stark_runner.rsstrict framing and seven-channel key layout
Converged native advicecrates/ixvm-codegen/src/aiur_ix_aggr_runner.rs, crates/ffi/src/aiur/protocol.rsstrict path framing, shared channel-6 key layout, execute/prove passthrough
Generated executorcrates/ixvm-codegen/src/aiur_multi_stark.rsgenerated artifact; verify with ix codegen --check
Converged generated executorcrates/ixvm-codegen/src/aiur_ix_aggr.rsgenerated artifact; verify with ix codegen --check
Manifest writercrates/kernel/src/shard.rsproducer-side omission, tree remap, aggregate metadata
Claim semanticscrates/kernel/src/claim.rs, crates/ixon/src/proof.rsfree-form root contract and codec regression test
TestsTests/Aggr.lean, Tests/AggrSemantics.lean, Tests/AggrActivation.lean, Tests/MultiStark.lean93 production shape/semantic/plan checks, 132-case activation audit, retained Ix-agnostic regressions
BenchmarksBenchmarks/AggregatePolicy.lean, Benchmarks/Typecheck.lean, bench registry/dashboardspinned four-shard wrap-first/direct handoff, resumable JSON, converged direct-shape-2 join metrics, historical lift-size pins

Current limits and non-goals

  • Recursion FRI parameters stay at the q=100 defaults; the q=50 + compensating
    PoW policy is an explicit follow-up decision (the measured costs above are
    therefore the conservative end).
  • Structural roots are manifest-relative. Environment-only verification
    accepts only a bundled value equal to the canonical environment root;
    hybrid roots should be verified with both --ixe and --ixes and the
    proving threshold.
  • Scheduler RAM weights are calibration-pending placeholders (validated in
    the safe direction by the measured run); concurrent proves share the rayon
    global pool, so throughput can sub-scale — process-per-slot is a
    measurement-driven follow-up.
  • Same-key cross-process cache writes degrade to a warning and recomputation,
    never a soundness issue.
  • Dummy-call activation padding is deferred until/unless a static terminal
    circuit is selected; under the SP1 terminal it is pure cost.
  • Aggregate wrappers reuse Ixon.Proof and do not encode the proof system;
    callers must pass ix verify --aggregate.
  • M1-a–f land the converged production circuit, host/native substrate, driver,
    cache, scheduler, value-based verifier, semantic union, widened activation
    audit, and policy benchmark harness. The only remaining M1 work is executing
    the wrap-first/direct comparison on the target box. The pinned M1-f revision,
    inherited M1-e preflight, and both measured policies are one checkout/session;
    its handoff is documented in
    plans/aggregate-first-m1f-large-box-runbook.md.
  • This PR does not adopt cold-circuit grouping and does not implement a
    terminal KZG/SP1 wrapper.

Follow-ups

  1. Calibrate lift/join unit costs on a known-core box, with per-node
    flat-vs-structural attribution, and measure frontier/discharge-path counts
    at Mathlib scale (plan §11.4.1).
  2. Decide the recursion q=50 + PoW policy (§9.5) now that the mechanism
    exists.
  3. Connect the root proof to the selected terminal compression path (E3:
    single-recursion-proof SP1 guest + ix compress-root).
  4. Re-evaluate cold-circuit grouping (E4); E2's measured outer-lift reduction
    was only 6.80%, so grouping may still be material.
  5. Process-per-slot proving if measured rayon contention warrants it.
  6. Run the exact M1-f commit
    b70042d6d1a029bb8e1d541da7aef5e0613d67e6 for the combined M1-e preflight
    and wrap-first/direct policy benchmark session in the large-box handoff
    runbook.

johnchandlerburnhamand others added 22 commits August 27, 2026 16:21
Add the deterministic lift/flat/structural activation matrix and keep dummy calls deferred. Split aggregate recursion commitment/FRI configuration from IxVM defaults, share it between proving and verification, and pin the future cache encoding without changing active protocol parameters.
Precompute versioned per-slot cache keys, persist lift and join wrappers in the content-addressed store, and reuse entries only after exact claim and native outer-proof verification. Add safe proof decoding, corruption recovery, and --no-cache.
Execute ready lift and join slots as a dependency DAG under explicit job and RAM admission. Add calibration-pending slot weights, failure draining, CLI controls, and serial/parallel scheduler and proof-equivalence gates.
Add an opt-in two-child benchmark that proves singleton CheckEnv shards, lifts both proofs, and measures a verified flat join. Wire join metrics through reporting and dashboards, and record the current pre-E2 lift-size baseline.
- rust-toolchain.toml channel: 1.92 → 1.98, with the matching fenix
toolchain hash in flake.nix. The pinned fenix already carries the 1.98
release manifest, so flake.lock needs no change (fenix's nixpkgs stays
pinned via its lean4-nix follows, so the Lean toolchain is untouched).
- Drop clippy::from_iter_instead_of_collect from the workspace lints:
removed in clippy 1.98 and now warns as unknown.
- Fix the warnings new clippy 1.98 lints surface across the workspace:
chunks_exact(N) → as_chunks::<N>() where the chunk size is constant,
descending sort_by → sort_by_key(Reverse(..)), iteration over map
values via .values(), map().unwrap_or() → map_or(), a checked
division, an unwrap-after-is_some restructured into if-let, and
assorted redundant-reference/pattern cleanups (mostly cargo clippy
--fix). The two byte-gadget files keep their chunks_exact warnings
until the next commit, which rewrites those regions anyway.
CI derives its Rust version from rust-toolchain.toml, so no workflow
changes are needed.
Companion to multi-stark's update-p3 branch (c72d321 → 249b740), which
carries four soundness/robustness fixes and the Plonky3 v0.6.0 bump
(pruned FRI Merkle multiproofs: ~2x faster verification, 40-70% smaller
proofs; canonical Goldilocks serde removes proof-byte malleability).
Proofs and verifying keys are not compatible with the previous pin.
Integration:
- Lookup gained max_multiplicity, a declared per-row bound on the
multiplicity's integer magnitude feeding the newly enforced logUp
height bound Σ wᵢ·hᵢ + |claims| < p. Function-circuit slots accumulate
mutually-exclusive branch selectors, so they declare 1; committed
count columns (function return slots, the memory circuit, the byte
gadget tables) declare the new COUNT_COLUMN_BUDGET (2^32 queries per
entry).
- The VK wire format carries the bound: u64 LE max_multiplicity per
lookup, between the multiplicity node id and the arg count. The
in-circuit VK deserializer (Ix/MultiStark/SystemDeserialize.lean)
parses past it; the value is bound through the vk digest but the
height bound itself is not yet enforced in-circuit (the native
verifier enforces it).
- Message fingerprints are width-bound by default upstream (the slot
width seeds the Horner fold), which is incompatible with aiur's
branch-shared lookup slots: mutually exclusive branches superpose
messages of different natural widths into one slot at the maximum
width, so a narrow call is sent zero-padded to a width its callee's
return slot never provides, and proving fails with
UnbalancedChannel (pinned as the prove_verify_mismatched_call_widths
regression). Aiur instead declares WidthBinding::ByConstruction —
the plain Horner fold, restoring zero-padding transparency — and
takes on the prefix-freeness contract that makes it sound: every
message's natural width is a function of its constant-constrained
leading prefix (channel tag plus discriminator: fun_idx fixes
2+in+out, the memory size coordinate fixes 3+size, each gadget tag
fixes its table width), so zero-extension can only equate a padded
message with its own natural form. The contract is documented at the
channel constants in lib.rs; the declaration is applied in
AiurSystem::build and mirrored in the vk_codec decoder so decoded
VKs replay the same transcript. ByConstruction is also exactly the
fold the in-circuit verifier's logup_fingerprint already computes,
so the recursive verifier needs no fingerprint change.
- The policy is Fiat-Shamir-bound as the first observe_shape word; the
in-circuit transcript replay prepends the matching limb.
- aiur_multi_stark.rs regenerated (ix codegen) for the deserializer
and transcript changes.
- P3 v0.6.0 ships FRI query openings as pruned Merkle multiproofs,
while the in-circuit verifier consumes one authentication path per
query (its per-query control flow is a far smaller circuit than the
amortized multiproof walk). Rather than porting the walk into the
DSL, the proof advice stays in the per-query transport: multi-stark's
new advice module re-encodes a natively-verified proof by running
p3's own verification with a recording compression function and
reading each query's path back out of the recorded digest map. The
advice bytes are untrusted verifier input, never digest-bound —
the transcript binds the commitments and every expanded sibling is
authenticated against them per query — so pruning vs expansion is
pure transport and the encoding choice is sound. AiurSystem gains
proof_to_advice_bytes (FFI: AiurSystem.proofToAdviceBytes); the
recursive-verifier test feeds it instead of Proof.toBytes, whose
native wire format is still round-tripped separately. The Lean-side
proof grammar and the codegen'd verifier are byte-identical to
before — no in-circuit changes.
Claim layout, the VK wire format above and aiur's public semantics are
otherwise unchanged; the policy adds no prover or verifier work over
the previous pin.
Still open, native-verifier-only: the logUp height bound is parsed
past but not yet enforced in-circuit (a wide-arithmetic check, tracked
separately).
The Lean v4.33.1 update pinned both dependencies at revisions that
predate their Rust 1.98 bumps; their heads now carry those bumps, which
this workspace needs since rust-toolchain.toml moved to 1.98. Both
revisions stay on leanprover/lean4:v4.33.1.
- Blake3.lean 1b0fbd2 → e6e908b (Rust 1.98, plus a case-insensitive
source-directory fix), updated in lakefile.lean, lake-manifest.json
and the blake3-lean flake input. The revision keeps the
`blake3_rs_shared` target the `ix_native_decide_dynlib` pin requires.
The inherited entry in Benchmarks/Compile/lake-manifest.json was still
on the pre-4.33.1 revision and now tracks the root pin.
- lean-ffi 2a9c91e → 93c7e52 (Rust 1.98). Only bignat reaches the sp1
and zisk workspaces, so their lock files move that one package.
The !benchmark recursive phase reported n/a for every fri-verifier
metric: Benchmarks/Typecheck.lean still fed Proof.toBytes — the pruned
multiproof wire format — to executeMultiStark/proveMultiStark, so the
in-circuit verifier rejected on parse and the harness (correctly) left
the recursive fields absent rather than emit a fake datum. The
in-circuit verifier consumes the per-query advice transport
(AiurSystem.proofToAdviceBytes); proofBytes stays the reported
proof-size metric.
bench-recursion-debug had the same advice-format gap plus a stale
claim recipe: it still built the public input as 32 raw digest bytes,
predating the ClaimHarness.packedDigestKey packing bench-typecheck
uses (its own out-of-circuit sanity check failed with
InvalidPowWitness — a wrong claim diverges every challenge — and the
advice re-encoder refused the proof for the same reason). Both aligned
with the typecheck flow.
Validated end-to-end at production parameters (numQueries 100,
query PoW 20, blowup 2) on Nat.add_comm: inner prove, advice
re-encoding, and the codegen'd in-circuit verifier accepting.
Keep compact proof bytes at storage and cache boundaries, expand them only for recursive lift/join advice, reject unsupported zero-query benchmarks, and decode legacy store proofs without panicking. Refresh tests, benchmark pins, and Rust 1.98 lint compatibility.
Add Ix/Aggr, a recursive aggregation system for IxVM shard proofs that
keeps Ix/MultiStark untouched and Ix-agnostic. One entrypoint, ix_aggr,
subsumes lifting and joining: a one-byte advice shape selects wrap or
binary join over any mix of IxVM and ix_aggr children, so shard proofs
enter the recursion system directly as join children and the dedicated
lift stage disappears.
Circuit (Ix/Aggr/Circuit.lean): every shape verifies its children in
full (verify + ood_verify from the shared Multi-STARK verifier modules)
against the vk its hinted kind demands. Identity is one 80-byte
digest-bound blob - blake3(ixvm vk) || verify_claim idx ||
blake3(self vk) || ix_aggr idx - carried unchanged at every node; self
children must bind the identical blob digest, pinning both vks and both
entrypoint indices transitively. Claims are a uniform 18-word
[0, aggr_idx, allowed(8), checkEnv(8)] at every depth, so proofs of
different tree levels combine freely. Wrap shapes bind the output
digest to the child CheckEnv digest directly; pair shapes open both
CheckEnv preimages, re-root the canonical subject/assumption trees, and
prove subjects = L ∪ R, assumptions = (asmL ∪ asmR) ∖ subjects with
linear sorted merges.
Toplevel (Ix/Aggr.lean): ixAggr = MultiStark.multiStarkFull + circuit,
pruned to ix_aggr, so verify_multi_stark_proof and other unrelated
entries no longer pad aggregate proofs. The host half of the wire
contracts (allowed blob, public input packing, shape codes, keyed
preimage/tree blob framing, interpreter IO assembly) lives beside the
toplevel; Ix/Aggr/Host.lean folds CheckEnvTrees statements.
Native path: ix codegen gains the ix-aggr target
(crates/ixvm-codegen/src/aiur_ix_aggr.rs, 244 fns); its runner builds
the seven-channel IO buffer natively and routes execution through the
generated code. New FFI rs_aiur_ix_aggr_execute/_prove
(executeIxAggr/proveIxAggr) pass proofs, vks, claims, and the compact
count/key/length preimage/tree blobs without per-byte boxing, plus
Proof.ofBytesChecked for store-boundary decoding.
CLI: ix aggr --ixe E --ixes M <shard-proof>... reconstructs every
nonempty shard statement from the env, matches wrappers by claim
digest, natively pre-verifies them, folds a balanced bisection (the
canonical fold makes the root claim independent of tree shape), wraps
single-shard roots so the persisted root is always an ix_aggr proof,
checks the root closes over the env canonical tree with no residual
assumptions, and persists the wrapper.
Tests (lake test -- ix-aggr, 17 cases): all five shapes accept over
real Multi-STARK stand-in child proofs from two distinct-vk systems;
codegen'd execution matches the interpreter on output and per-circuit
query counts for wrap and pair; negatives break one binding each -
lying shape hint, tampered proof, foreign identity, wrap statement
drift, dropped assumption, padded subject set, and tree advice not
reproducing its keyed root.
Replace the legacy lift/join entrypoints with the heterogeneous ix_aggr system across aggregate, verification, cache, and codegen paths. Preserve wrap-first and direct-join policies behind one proof identity and cache namespace.
Add the converged 91-check semantic suite and a deterministic 132-case activation audit covering shapes 0 through 9 twice, with no unobserved circuits.
Replace the legacy two-shard three-entrypoint benchmark with a four-shard manifest-subtree harness for production ix_aggr wrap-first and direct policies. Enforce the q=100 serialized no-cache profile, natively verify every input and recursive output, persist ordinary aggregate wrappers, and emit resumable per-slot JSON metrics.
Port bench-typecheck --join to ix_aggr direct shape 2 while preserving the stable join metric schema. Remove the final legacy slot-spec/preimage shim and pin both M1-f four-shard plans in the focused suite.
Update the multi-stark dependency and Rust toolchain for Plonky3 0.6, along with the Rust 1.98 lint migrations required to keep the workspace warning-free. Refresh the Rust-compatible Blake3.lean pin in both root and compile-package manifests.
Adapt recursive Aiur verification to Plonky3's pruned FRI multiproofs. Native proofs retain their compact serialized representation and native verification path; the FFI expands authenticated Merkle frontiers into per-query advice only when entering the existing recursive verifier circuit.
Preserve the packed claim-digest convention in the recursion diagnostic and exercise the advice boundary in the end-to-end test and benchmark paths. CPU and CUDA recursive q1 runs produce identical 823,485-byte inner proofs and 331,273-byte outer proofs.
The q50 Vector.extract_append workload retains identical CPU/CUDA proof sizes. Inner plus outer STARK proving measures 65.87s on CPU and 8.81s with CUDA on the RTX PRO 6000, a 7.48x speedup.
PR benchmark runs execute trusted workflow YAML from the default branch while loading composite actions from the PR checkout. When Bencher data and binary caches are unavailable, the workflow checks out main under base/ and asks Lake to rebuild it without first installing the Rust channel pinned by that checkout.
Teach the existing CPU provenance action to install the base checkout's validated Rust channel and profile immediately before an uncached base build. The step is a no-op when the toolchain is already available and leaves cached benchmark comparisons unchanged.
Consume the Plonky3 0.6 batch-opening layout directly in Aiur instead of expanding every pruned Merkle frontier into one authentication path per FRI query. Sample all query indices from the unchanged transcript, sort and deduplicate them with an O(q log q) merge sort, authenticate each input and commit-phase commitment once, then retain the existing per-query reduced-opening and FRI arithmetic.
Bind every frontier to transcript-derived indices, consume boundary digests in Plonky3's level/parent/child order, reject trailing frontier elements and inconsistent duplicate leaves, and assert all native opening dimensions and sibling counts. Explicitly constrain the digest-bound protocol specialization to cap height 0, binary FRI, and a constant final polynomial. Move memo_u32_less_than into IxVM Core so both substitution and multiproof sorting share its constrained rows.
Strengthen the recursive negative test to mutate a structurally valid stage-1 commitment. Regenerate both checked-in Aiur Rust executors and retain interpreter/codegen query-count parity.
On Vector.extract_append q50, recursive-verifier FFT cost falls from 204.073B to 201.166B. CPU outer proving improves from 50.09s to 45.03s and the full CPU pipeline from 90.64s to 82.90s. GPU outer proving improves from 15.85s to 13.72s and the full GPU pipeline from 28.86s to 26.69s. The outer proof grows from 3.92 MB to 4.17 MB.
Validated with the MultiStark primitive suite, recursive honest/tamper/parity tests, codegen --check, release workspace clippy, release CUDA clippy, rustfmt, and diff checks.
@johnchandlerburnham

Copy link
Copy Markdown
MemberAuthor

Init full proving report

Revision: 82dcc9dba36d0cfd6c753c6878ab649643a52026
Host: 64 vCPU, 495 GiB RAM, 256 GiB swap
Final result: one verified ix_aggr FRI proof with no remaining assumptions.

Compilation and sharding

StepResult
Compile sourceBenchmarks/Compile/CompileInit.lean
Ix compiler time1.876s
Constants compiled65,994
init.ixe size194,296,853 bytes (185.3 MiB)
Static sharding time3.1s
Manifest16 nonempty shards
Retained/owned constants56,621
Manifest size2,619,404 bytes (2.50 MiB)
Predicted FFT per shard4.160e11–4.197e11
Predicted FFT spread1.01×

The 1.876-second compile measurement is the Ix compiler itself; it excludes the initial one-time dependency download and Lake build.

Stage 1: base shard proofs

All 16 shard proofs were persisted and individually verified against their corresponding manifest claims.

MetricSerial baselineTwo-way run used for aggregation
Concurrency12
End-to-end wall time24m 16.76s15m 53.93s
Sum of shard prover times22m 43.23s30m 08.14s
Per-shard time range74.08–113.97s98.94–142.29s
Median shard time82.66s108.75s
Highest individual RSS219.8 GiB219.8 GiB
Combined process-tree peak358.2 GiB
Proof count1616
Total proof-wrapper size369,995,038 bytes
Mean proof-wrapper size23,124,690 bytes
Proof-wrapper range22,714,520–23,998,692 bytes

Two-way concurrency gave a 1.53× wall-time speedup despite increasing total prover-seconds by about 33% from CPU/memory contention.

Stage 2: recursive aggregation

Policy:

  • Wrap-first
  • --structural-above 4096
  • --jobs 2
  • --max-ram 450
  • Default q=100 recursion parameters
  • Aggregate cache enabled

Plan:

OperationCount
Shard wraps16
Binary joins15
Structural joins14
Flat joins1
Total recursive slots31

Results:

MetricResult
Wall time45m 06.61s
Peak resident memory379.9 GiB
Average CPU utilization4,192% (~42 cores)
Cache hits0
Freshly proved/cached slots31
Swap used0 bytes
Exit status0
Final proof-wrapper size9,596,044 bytes (9.15 MiB)

Final proof

Root address:

80f6844f968920abaef8d8c750a1a1dc1ebe32657a3cfb824e1810bcd65c8814

Final claim:

CheckEnv(6dc8f821a989c6f7b9d520b053ef8edbd582d2408d7b6a6ff72b908666e03b6f, none)

Both validations passed:

  • Cryptographic-only aggregate verification
  • Manifest-aware verification against init.ixe, init-16.ixes, and structural threshold 4096

End-to-end measured time

Using the two-way Stage 1 run:

ComponentTime
Ix compilation1.876s
Sharding3.1s
Stage 1 proofs15m 53.932s
Stage 2 aggregation45m 06.61s
Measured core pipeline1h 01m 05.52s

This total excludes the one-time dependency build/download and a few seconds of manifest and final-root verification overhead. Using serial Stage 1 instead would make the measured core pipeline approximately 1h 09m 28.35s.

Use the PR77 lean-ffi and multi-stark pins, remove the stale width-binding and multiplicity-bound protocol fields, and regenerate the recursive and aggregate verifier sources.
Expose an opt-in x86_64-linux cuda shell with NVCC 13.2, sm_120 code generation, and safe host libcuda discovery while leaving the default development shell unchanged.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@johnchandlerburnham@samuelburnham@arthurpaulino
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root - #598

Open
johnchandlerburnham wants to merge 24 commits into
mainfrom
jcb/aggregate-first
Open

Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root#598
johnchandlerburnham wants to merge 24 commits into
mainfrom
jcb/aggregate-first

Conversation

@johnchandlerburnham

@johnchandlerburnhamjohnchandlerburnham commented Aug 27, 2026

Copy link
Copy Markdown
Member

Summary

This PR implements the aggregate-first path for Aiur Multi-STARK proofs. The
production path now runs through one ix_aggr recursion entrypoint: it wraps
independently-proven IxVM shards, folds their CheckEnv statements along the
manifest's bisection tree, and persists one recursive root proof with a
uniform 18-word outer claim. Every completed recursive slot is persisted in a
verified, content-addressed version-2 resume cache, and ready slots are proven
in parallel under explicit job and RAM limits.

The entrypoint has two pair-fold modes:

  • Flat shapes perform a canonical set union/difference and are efficient for
    small lower nodes.
  • Structural shapes commit to nodeHash(leftRoot, rightRoot) in one hash and
    discharge assumptions with Merkle inclusion paths. Their 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 (default
4096 subject leaves): flat below the threshold, structural above it.

IxVM shard proofs
│
├── ix_aggr shape 0 (default wrap-first policy)
│ or remain raw with --direct-joins
▼
ix_aggr proofs carrying uniform CheckEnv statements
│
├── flat ix_aggr shapes 2–5 below the configured subject threshold
│ subjects := canonical(subjectsL ∪ subjectsR)
│ assumptions := (assumptionsL ∪ assumptionsR) ∖ subjects
│
└── structural ix_aggr shapes 6–9 above the threshold
subjects := nodeHash(subjectRootL, subjectRootR)
assumptions := candidates carried or discharged by inclusion path
▼
Ixon.Proof {
claim := CheckEnv(manifest-relative subject root, none),
proof := one recursive Multi-STARK root proof
}

A manifest with one retained (nonempty) shard is also a valid deliverable: its
root is one shape-0 wrapper, with no self-join padding (including under
--direct-joins). Zero-constant manifest leaves are pruned after raw coverage
validation:

raw manifest
|
| validate exact environment coverage before pruning
v
drop zero-constant leaves + contract unary tree nodes + remap retained ids
|
+-- one retained leaf --> wrap once and persist the ix_aggr root
|
`-- multiple leaves --> fold flat/structural joins as above

This is the stage-2 aggregation layer from
plans/aggregate-first-pipeline.md: shard proofs are aggregated before any
terminal KZG/SP1 compression, so a later wrapper only needs to consume one root
proof. Terminal compression remains outside this PR.

Section 14 implementation is complete through M1-f in this branch. The
ap/ix-aggr single-entrypoint base is reconciled with WP-E2's compact stored
proof / expanded recursive-advice boundary, and its one ix_aggr entrypoint
supports wraps, flat pairs, and structural pairs across both IxVM and recursive
children. ix aggregate now uses that backend for planning, proving, cache
resume, and final verification; ix verify --aggregate derives one uniform
outer claim and performs value-based environment/manifest checks. The old
ix aggr command and all three-entrypoint production wiring are retired. The
older Multi-STARK aggregate implementation remains as an Ix-agnostic regression
surface, not as a CLI backend. Production semantics, activation coverage, and
benchmarking now live entirely against ix_aggr. M1-f lands the pinned
four-shard bench-aggregate-policy handoff and ports the stable typecheck join
metric to direct shape 2; only execution of the policy comparison on the large
box remains. Operationally this is one future handoff: Arthur will check out
b70042d6d1a029bb8e1d541da7aef5e0613d67e6 once, run the inherited M1-e
correctness suite as untimed preflight, and then benchmark both policies. There
is no separate M1-e box run.

Measured performance (2026-08-29)

Arthur Paulino ran the pre-convergence pipeline end-to-end on four real Init
shard proofs (shards 8–11 of a 16-way partition, 12,493 constants; base shard
proving excluded; native verification of each output included; ~512 GB box).
These are the standing M1-f comparison baseline; the converged wrap-first and
direct policies have not yet been rerun on that box. The recursion used the
conservative q=100 / PoW 20 defaults this PR keeps:

StageTimePeak RAMOutput proof
Lift × 498.8–103.0 s (mean ≈ 101 s)186.8–195.8 GiB8,162,462 B each
Join 8+9, 10+1148.9 / 48.5 s~102.5 GiB9,502,843 B
Root join85.8 s156.9 GiB9,455,498 B
Total (serialized)10:23 wall195.8 GiB9,455,498 B

Notes: lift cost is shard-content-independent (four lifts within ±2%, tracking
the ~constant 20–23 MB shard-proof shape); the measured lift peak brackets the
scheduler's 195 GiB weight placeholder exactly; and an unserialized run of all
four lifts OOM'd at ~492 GiB — precisely the behavior the WP-B admission gate
prevents (on a 512 GB box its weights admit two lifts). Because shards 8–11
are a strict partition subset, the root carries a real frontier assumption
set, so the joins exercised genuine discharge work. Full analysis, including
the comparison against direct (unlifted) IxVM joins, is in
plans/aggregate-first-pipeline.md §3.4 and the Zulip thread.

What lands

Protocol / circuit layer:

  • One production recursion entrypoint, ix_aggr, under one verifying key.
    Shapes 0–1 wrap one IxVM/recursive child, shapes 2–5 fold flat pairs, and
    shapes 6–9 fold structural pairs for every child-kind combination.
  • Recursive validation of wrap, flat-pair, and structural-pair children with
    transitive verifying-key, function-index, and allowed-system binding. A
    shape hint selects the verified form but cannot weaken its checks.
  • Strict in-circuit CheckEnv decoding and canonical flat set folding.
  • Structural subject folding with per-candidate Merkle discharge paths while
    keeping output assumption sets canonical and deduplicated.
  • One 80-byte protocol identity containing the IxVM/self verifying-key
    digests and their two accepted entrypoint indices; every recursive node has
    the same 18-word outer-claim layout regardless of shape.
  • Structural outputs bind nodeHash(leftRoot, rightRoot) and account for each
    unique assumption candidate through a strict Merkle-path-or-carry choice.
  • Host helpers construct structural roots, survivors, and path advice. Channel
    6 carries both the one-byte shape at key [0] and candidate path payloads at
    raw 32-byte address keys; the key shapes are disjoint.
  • Lean/Rust execute and prove FFI accept a compact keyed path blob, whose
    framing is decoded strictly before the circuit independently validates
    payload semantics.
  • The generated ix_aggr executor is regenerated at 2,061,361 bytes / 248
    Aiur functions and parity-tested against the interpreter.

Host driver and operations:

  • Strict .ixes aggregation-tree parsing and post-order lowering, with a
    balanced fallback for legacy manifests; validate-then-prune removal of
    zero-constant leaves with unary tree contraction (and a Rust writer that
    stops emitting them).
  • Single-shard roots: a one-retained-shard manifest always produces one
    shape-0 ix_aggr wrapper, including under --direct-joins.
  • A monotone leaf-count scheduler controlled by --structural-above, with a
    default wrap-first plan (shapes 0 then 5/9) and an explicit
    --direct-joins plan that derives heterogeneous shapes from actual child
    kinds.
  • Versioned, content-addressed aggregate cache and resume (WP-A): version 2
    binds the uniform outer claim, and reuse occurs only after content-digest,
    claim, decode, and native verification all pass; --no-cache bypasses.
  • Parallel execution of ready wraps/pairs as a dependency DAG under --jobs
    and --max-ram (default 92% of MemTotal), heaviest-first, with failure
    draining (WP-B). Admission weights are shape-aware: 4 GiB for a retained
    raw IxVM leaf, 195 GiB for wraps/self pairs, 390 GiB for IxVM/IxVM pairs,
    and 340 GiB for mixed pairs.
  • A recursion-parameter split (WP-E1): aggregate proving/verification share an
    explicit RecursionParameters value, defaulting to today's q=100 values;
    the q=50+PoW policy decision is deliberately deferred.
  • A deterministic 132-case production activation audit across shapes 0–9:
    two controlled child trace heights, both wraps, every flat/structural
    child-kind pair, optional assumptions per side, and discharge/carry. The
    complete matrix runs twice with a stable signature and observes every
    catalogued circuit; dummy-call padding remains intentionally deferred.
  • Claim-only shard preparation (no discarded dependency-byte closure walks)
    with a one-pass multi-root witness closure for proving.
  • ix aggregate --ixe E --ixes M [--structural-above N] [--direct-joins] [--jobs N] [--max-ram G] [--no-cache] [--plan-only] <proof>... and
    single-entrypoint aggregate verification with cryptographic-only,
    environment-bound, and manifest-bound value checks. The duplicate
    ix aggr command is removed.
  • Opt-in aggregate benchmarks: a tiny two-shard Init pair end-to-end bench and
    a flat-join bench (singleton CheckEnv shards → two lifts → verified flat
    join), with join metrics wired into reporting/dashboards, the pre-E2 lift
    proof size pinned at 7,986,166 bytes, and the post-E2 delta measured.
  • Positive, transitive-recursion, scheduler, cache, pruning, parity,
    malformed-input, and semantic-tampering tests: the aggregate-first suite
    has 58 cases, plus the separate activation matrix.

Protocol design

One recursion system, one entrypoint

Production builds Aggr.ixAggr, the shared Multi-STARK verifier closure pruned
to one ix_aggr entrypoint. A one-byte advice hint selects the exact verified
shape:

ShapeChildrenFold
0IxVMpass-through wrap
1ix_aggrpass-through wrap
2–5IxVM/ix_aggr pair (2 + 2·left + right)canonical flat fold
6–9IxVM/ix_aggr pair (6 + 2·left + right)structural fold

The hint is advice, not authority: each arm verifies the child proof(s) under
the key required by the encoded child kinds, then requires that kind's exact
claim layout. A wrong shape therefore fails proof verification, function-index
binding, or claim decoding.

Uniform public statement and pinned protocol identity

Every wrap and pair exposes the same public-input shape:

ix_aggr(allowed_digest: [G; 8], out_claim_digest: [G; 8])

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:

  1. the 80-byte allowed-system blob; and
  2. the serialized output CheckEnv claim.

The allowed blob is:

blake3(ixvm_vk)
|| verify_claim_idx:u64le
|| blake3(ix_aggr_vk)
|| ix_aggr_idx:u64le

The circuit requires exactly 80 bytes. 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
another function in those systems; binding the two accepted 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.
Every persisted aggregate outer claim is consequently the same 18-word form:
the ix_aggr function selector followed by the packed identity and output
claim digests. Shapes are witness choices and do not become root-kind metadata.

Child-proof validation and transitive pinning

Each pair reads the verifying key required by each child kind, checks its
Blake3 digest against the allowed blob, and deserializes it strictly. Wraps do
the same for their one child. For every child the selected arm:

  1. reads and fully consumes one proof and exactly one outer claim;
  2. runs the Multi-STARK structural verifier;
  3. runs ood_verify, including Fiat-Shamir replay, lookup accumulators,
    quotient/OOD checks, Merkle openings, and FRI verification; and
  4. decodes the statement according to the constrained child kind and function
    index.

The accepted forms are:

  • IxVM child: its system digest must equal blake3(ixvm_vk). Its claims
    digest opens to exactly one 10-word IxVM claim at the pinned
    verify_claim_idx, whose digest opens to a strict serialized CheckEnv
    claim.
  • ix_aggr child: its 18-word claim must use the pinned ix_aggr_idx; its
    first public digest must equal the current allowed_digest, transitively
    pinning both systems and both entrypoints; and its second digest opens to its
    output CheckEnv claim.

Any other index, shape, digest, trailing byte, or failed proof check rejects.
This keeps wrap→pair, flat→pair, and structural→structural composition on one
statement format.

Flat canonical folding

For child statements (S_L, A_L) and (S_R, A_R), flat shapes 2–5 enforce:

S_out = S_L ∪ S_R
A_out = (A_L ∪ A_R) ∖ S_out

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 u32 words using the
full-domain u32_less_than primitive. The implementation never uses Aiur
pointer 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

Structural shapes 6–9 perform the same allowed-blob binding, child-key
binding, two child-proof verifications, child decoding, and output-claim
binding as the flat shapes. They replace all subject-tree loading and set
re-rooting with:

S_out.root = nodeHash(S_L.root, S_R.root)

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:

  • carried (0): no bytes may follow, and the candidate must be the next
    value in the output assumption list; or
  • discharged (1): a bounded Merkle path from
    leafHash(candidate) to S_out.root must verify.

Path payloads are strict:

0
1 || count:u8 || repeated(side:u8 || sibling[32])

count is limited to 64. side = 0 hashes nodeHash(sibling, current) and
side = 1 hashes nodeHash(current, sibling); any other side rejects. The
payload 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

All ix_aggr shapes use the same seven-channel IO layout. Digest/root bindings
are checked before the corresponding bytes are decoded.

ChannelKeyValue
0[0], [1]first and optional second child proof advice bytes
1[kind]verifying-key bytes (0 = IxVM, 1 = ix_aggr)
2[0], [1], [2]left/right outer claims and output CheckEnv claim
3[0]80-byte allowed-system blob
4packed Blake3 digestnested claim preimages
5raw 32-byte addressserialized canonical trees keyed by root
6[0] / raw candidate addressshape byte / carried-or-discharged path choice

Lean passes channels 4–6 across FFI as compact framed blobs:

count:u32le || repeated(key[32] || payload_len:u32le || payload)

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.

Wraps receive no tree or path advice. Flat pairs receive full
subject/assumption tree advice and an empty path blob. Structural pairs receive
only the input/output assumption trees plus one path choice for every
deduplicated candidate; subject trees are never opened.

Manifest and host pipeline

.ixes aggregation tree

parseIxesManifest exposes both shard block lists and the optional binary
aggregation tree stored at the manifest tail. It:

  • requires nonzero, contiguous shard IDs;
  • validates option and tree tags;
  • requires tree leaves to be exactly the manifest shard set;
  • rejects duplicate/out-of-range leaves and trailing bytes; and
  • synthesizes a balanced ascending-ID tree for legacy manifests without a
    tree tail or with an explicit absent-tree tag.

The tree lowers to post-order FoldOp slots. Every join refers only to earlier
slots and the last slot is the root, providing a direct serial schedule and the
basis for cache keys and parallel execution.

Validate first, then prune empty manifest leaves

IxesManifestView retains the original shard id for every dense shard slot.
pruneEmpty:

  1. counts the environment constants owned by every raw shard;
  2. drops only shards with a count of zero;
  3. contracts aggregation-tree nodes with a removed child;
  4. densely remaps retained leaf indices; and
  5. returns the retained per-shard subject counts with the pruned view.

The aggregate and manifest-aware verifier paths deliberately call
shardsCover on the raw manifest before this transformation. Coverage
still establishes that every environment constant is owned exactly once. The
subsequent zero count is therefore evidence that a removed leaf contributes no
subject; pruning is not allowed to hide a missing or duplicated constant.

Original shard ids are preserved separately from dense scheduling ids, so CLI
plans and proof-binding errors continue to identify the source manifest shard.
An all-empty manifest is rejected because it cannot produce a root.

For a tree node node(left, right), pruning follows:

both retained -> node(left', right')
only left retained -> left'
only right retained -> right'
neither retained -> removed

The Rust .ixes writer (crates/kernel/src/shard.rs) also normalizes new
manifests before serialization — block-empty shard records are omitted, ids
rewritten densely, the tree pruned/contracted/remapped, and cross-ingress
recomputed. The Lean-side pass remains necessary for legacy manifests and for
the stronger environment-relative condition (a shard may contain blocks yet
own zero constants).

Monotone structural scheduling

The host counts actual owned constants for each shard after the disjoint-cover
check. schedulePlan annotates every slot with its cumulative subject count and
chooses a structural pair exactly when:

left.subjectCount + right.subjectCount > structuralAbove

The default threshold is 4096; --structural-above 0 makes every join
structural 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.

By default, every leaf is an ix_aggr shape-0 wrap, so binary slots are shape
5 below the threshold and shape 9 above it. With --direct-joins, a
multi-shard plan keeps leaves as raw IxVM children and derives each pair shape
from its actual child kinds: 2–5 for flat pairs or 6–9 for structural pairs.
The monotone threshold rule is unchanged.

The proving loop checks reconstructed subject counts against the schedule
before starting expensive work.

Singleton roots

ix aggregate imposes no two-shard minimum. The fold plan for one retained
leaf contains one shape-0 wrap and zero pairs; that wrapper and its CheckEnv
claim are persisted as the aggregate root. Even --direct-joins wraps a
singleton because the deliverable must verify under the one production
entrypoint. A self-pair would add work and manufacture a different statement.
Consequences:

  • exactly one proof is required per nonempty shard;
  • --plan-only reports one wrap and zero binary pairs for a singleton;
  • multi-shard flat/structural scheduling is unchanged; and
  • the final unconditional-root check always applies to an ix_aggr root.

ix aggregate

ix aggregate --ixe ENV.ixe --ixes SHARDS.ixes \ [--structural-above N] [--direct-joins] [--jobs N] [--max-ram G] [--no-cache] \ SHARD_PROOF_ADDR...

The command:

  1. deserializes the environment and manifest and verifies a disjoint exact
    shard cover on the raw manifest, then prunes zero-constant leaves;
  2. reconstructs every retained shard's exact CheckEnv claim and canonical
    trees (claim-only preparation — no dependency-byte closure);
  3. matches proof arguments to shards in any order by bundled claim digest,
    then requires exact claim equality and one proof per nonempty shard;
  4. natively verifies every IxVM shard proof before expensive recursion work;
  5. builds the IxVM and ix_aggr recursion systems and the pinned 80-byte
    allowed blob;
  6. derives every slot's statement, child kind, shape, uniform outer claim, and
    version-2 cache key up front;
  7. executes ready wraps and flat/structural pairs as a RAM-gated dependency
    DAG, resuming verified cached slots and persisting each completed wrapper
    (raw direct-policy leaves are verified inputs, not cache slots);
  8. canonicalizes all leaves under the final subject tree and checks that they
    reproduce the environment's canonical root;
  9. requires no remaining assumptions and natively verifies the outer proof at
    the one ix_aggr root entrypoint; and
  10. persists an Ixon.Proof containing the output CheckEnv claim and proof.

--plan-only stops after coverage validation and scheduling, resolving and
printing the scheduler policy without loading proofs or compiling recursion
systems:

ix aggregate --ixe ENV.ixe --ixes SHARDS.ixes \ --structural-above 0 --plan-only

ix verify --aggregate

Aggregate roots reuse Ixon.Proof, so verification is selected explicitly.
The verifier never accepts root-kind metadata. It serializes the bundled
CheckEnv value and reconstructs the only valid outer claim:

claim_bytes := serialize(CheckEnv(...))
outer_claim := ix_aggr(
blake3(allowed_blob),
blake3(claim_bytes))
InvocationCryptographic entrypointExpected statement value
ix verify --aggregate <proof>ix_aggrbundled claim; cryptographic proof only
ix verify --aggregate --ixe E <proof>ix_aggrcanonical environment root, unconditional
ix verify --aggregate --ixe E --ixes M <proof>ix_aggrpruned manifest fold, unconditional

Environment-only mode compares the bundled value to the canonical environment
root; a manifest-relative structural value therefore rejects naturally, with
no root-kind branch. Manifest-aware verification repeats pruning and the fold
schedule (the threshold must match the proving run), reconstructs every leaf
statement, folds the exact hybrid value, requires no assumptions, and verifies
the same ix_aggr outer claim. Existing non-aggregate proof and
shard-composition verification paths are unchanged.

Claim-only shard preparation

shardCheckEnvClaimTrees constructs only the CheckEnv claim and its
canonical subject/assumption trees. Aggregate planning, shard-digest
reconstruction, and aggregate verification use this path because none of them
consumes dependency bytes. shardCheckEnvClaim remains the witness-building
entry point; it reuses the same claim/tree result and computes the byte
closure once from the union of the owned and available primitive roots.
Reachability distributes over that root union, so this preserves the old
closure while avoiding repeated traversal of shared subgraphs.

Recursion parameters (WP-E1)

Aggregation and aggregate verification consume one shared
MultiStark.RecursionParameters value containing the recursion commitment and
FRI configurations, carried through runAggregateCmdWith/runVerifyCmdWith
into the shared MultiStark.buildRecursionSystem. The CLI entrypoints pass
defaultRecursionParameters, whose fields are exactly today's canonical Aiur
defaults. This is a mechanism split, not a policy change:

  • ordinary IxVM proving and verification remain on the canonical q=100
    configuration;
  • recursion proofs also remain q=100 with the same commitment and PoW values;
  • no new CLI flag selects security parameters; and
  • choosing q=50 plus a compensating PoW policy remains the explicit §9.5
    follow-up decision.

The recursion verifying key serializes and binds both parameter sets, and its
digest is included in every aggregate node's allowed-system blob, so any parameter
change changes the recursive protocol identity without a public-input or
allowed-blob format change. RecursionParameters.cacheFriBytes pins the
40-byte fri_params_ser cache-key component:

logFinalPolyLen:u64-le || maxLogArity:u64-le || numQueries:u64-le ||
commitProofOfWorkBits:u64-le || queryProofOfWorkBits:u64-le

Commitment parameters need no second cache component: changing them changes
the recursion-vk digest already present in the key.

Pruned FRI multiproofs (WP-E2)

The four-commit origin/update-multi-stark series is transplanted onto this
stack: Rust 1.98, the 249b7405 multi-stark revision on P3 v0.6, the matching
Blake3/Lean-FFI pins, and recursive proof-advice encoding. The new proof wire
format deduplicates shared Merkle siblings across sampled FRI queries. Width
and trace-height binding and the vk's checked max_multiplicity travel with
the dependency update; no aggregate claim or public-input format changes.

There are now deliberately two proof encodings. Proof.toBytes is the compact
wire form used by wrappers, native verification, the store, and WP-A cache
entries. Recursive execution still expects one path per query, so callers use
AiurSystem.proofToAdviceBytes immediately before a wrap or pair. The CLI,
pair/typecheck benchmarks, semantic tests, and activation audit all enforce
that boundary. Cached proofs stay compact at rest and are expanded only when
consumed as a parent pair's child.

The dependency's compact format is not backward-decodable. Store-facing CLI
verification therefore uses Proof.ofBytesChecked and reports a normal error
for legacy or corrupt proofs instead of crossing the unchecked decoder and
aborting. P3 v0.6 also requires a positive FRI query count; benchmark CLIs
reject --queries 0, and the old q=0 join smoke gate is replaced by q=1.
The Rust 1.98 migration also applies its new strict-Clippy equivalents for
fixed-size digest chunking and fallible metadata-size lookup; these are
mechanical, semantics-preserving rewrites.

Verified cache and resume (WP-A)

Before the first proof, the driver derives every fold slot's CheckEnv
statement, exact outer Aiur claim, and cache key, using the same
flat/structural fold policy as proving — so a manifest rebudget or statement
change invalidates only the changed subtree. The key:

blake3(
aggregateCacheVersion:u64-le ||
blake3(recursion_vk) ||
recursion_fri_params:40-bytes ||
serializeClaims([outer_claim])
)

The uniform outer claim already includes the ix_aggr entrypoint index and
public input, hence the allowed-system blob, output CheckEnv, both
verifying-key identities, and both accepted entrypoint indices. Each completed
recursive slot is stored as an Ixon.Proof wrapper in the content-addressed
~/.ix/store; a raw --direct-joins leaf is only an input and is not cached.
The wipeable index ~/.ix/cache/aggregate/<key> holds only the wrapper's store
address and is updated via temp-file + atomic rename. M1-d bumps
aggregateCacheVersion from 1 to 2, so old outer-claim entries miss cleanly.

A hit is an untrusted hint, not authority. Reuse requires all of: a
well-formed index address; store bytes whose Blake3 digest equals it; a
decodable wrapper; a bundled claim exactly equal to the precomputed slot
CheckEnv; a safely decoded Aiur proof (Aiur.Proof.ofBytesChecked — the
former trusted-byte constructor panicked on truncated bincode, and malformed
cache bytes must never abort the command); and native verification under the
exact precomputed outer claim and local recursion system. Any failure logs a
miss and re-proves. Cache write failures are warnings. On a valid hit the
host reconstructs the slot's preimages from the manifest statement, so
parents need no trusted metadata from the cache. --no-cache bypasses index
reads and intermediate writes while still persisting the final root wrapper.

RAM-gated parallel scheduling (WP-B)

The post-order fold executes as a dependency DAG: leaves are ready
immediately; a join becomes ready when both child slots have completed. The
controller sorts ready work by descending RAM weight (slot number breaks
ties) and starts one dedicated IO.asTask per admitted slot around the
existing proof FFI. Two independent limits govern admission:

  • --jobs N caps active slots; absent or 0 means every ready slot may run
    if it fits the budget;
  • --max-ram G caps total reserved GiB in flight, defaulting to 92% of Linux
    MemTotal.

As in the existing Rust RamGate, a slot whose estimate exceeds the entire
budget is admitted only while nothing else runs — a conservative estimate
serializes work rather than deadlocking. The calibration-pending per-shape
weights are:

raw IxVM leaf = 4 GiB
shape 0/1 wrap = 195 GiB
shape 2/6 IxVM + IxVM = 390 GiB
shape 3/4/7/8 mixed pair = 340 GiB
shape 5 self + self flat = 195 GiB + 1 MiB × subject leaves
shape 9 self + self struct = 195 GiB

The 195 GiB wrap reserve carries forward the recorded q=100 lift upper bound —
the measured peaks above (186.8–195.8 GiB) bracket it exactly. The 390/340 GiB
direct and mixed estimates deliberately keep the non-default policy out of the
256 GiB fleet tier pending M1-f calibration; measured self-pair peaks
(102.5–156.9 GiB) make their weights conservative in the safe direction. Real
calibration on a known-core box will tighten them without changing scheduler
semantics.

Results are installed by slot index, never arrival order; each worker gets an
immutable snapshot of its completed children, so parallel timing cannot
change parent advice. On failure the controller stops admitting, drains every
running proof, and reports the lowest failed slot; successful independent
tasks may finish publishing valid cache entries. Concurrent proofs share
Aiur's rayon global pool, so throughput can sub-scale even when memory
permits multiple slots; process-per-slot execution is a measurement-driven
follow-up.

Converged activation audit (WP-D + M1-e)

lake exe IxTests aggregate-activation is a dedicated diagnostic kept out of
the default suite. It executes the generated production ix_aggr path across
4 wraps and 128 pairs: two controlled child trace heights, shapes 0–9,
optional assumptions independently per side, complete discharge versus
explicit carry, and all four child-kind combinations in both flat and
structural modes. Every execution returns per-circuit (uniqueRows, totalHits); the runner performs the full 132-case matrix twice and rejects any
difference before rendering a deterministic per-shape Markdown report.
Current signature:

263a3087d42340613b012d50a9416e23549f85aa1ea425e86bf0a216c7b3e27d

Result: 183 audited circuits — 154 active in every case, 29 input-dependent,
zero never observed. The variable set concentrates in the expected paths:
wrap dispatch, canonical set folding, structural Merkle discharge, and
child-kind decoding. Fixed-height Bytes1/Bytes2 circuits are
outside the execute FFI's query-count array and are called out explicitly.
The audit records information only; dummy calls would add cost for the
currently planned SP1 terminal and should be chosen only if a future static
terminal requires input-independent activation.

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.executeMultiStarkJoin for generated or interpreted flat
    and structural join execution;
  • AiurSystem.proveMultiStarkJoin for witness generation and proving;
  • JoinAdvice, JoinPreimage, JoinTree, JoinPath, strict framed decoders,
    and the seven-channel join_io_buffer;
  • the matching rs_aiur_multi_stark_join_execute and
    rs_aiur_multi_stark_join_prove externs; and
  • Aiur.Proof.ofBytesChecked for panic-free store-boundary proof decoding.

The retained three-entrypoint executor is parity-checked against the bytecode
interpreter on both output and every circuit's unique-row/total-hit counts. The
combined verifier contains 247 Aiur functions; the regenerated Rust file is
2,239,894 bytes (ixvm: 5,807,375 bytes / 780 functions). Most line churn is
generated code. The protocol source of truth is
Ix/MultiStark/Aggregate.lean; the native advice contract lives in
aiur_multi_stark_runner.rs. It is now an Ix-agnostic regression/reference backend,
not the production CLI backend.

The production ix_aggr executor is independently parity-checked for wrap,
flat, and structural shapes. It contains 248 Aiur functions and its generated
Rust file is 2,061,361 bytes. Its source of truth is
Ix/Aggr/Circuit.lean, with native advice construction in
aiur_ix_aggr_runner.rs; all three generated artifacts are checked with
ix codegen --check.

Benchmarks

M1-f replaces the legacy two-shard/three-entrypoint runner with
bench-aggregate-policy, a production ix_aggr benchmark for one exact
four-shard fixture. It selects the four retained leaves from a validated and
pruned manifest, contracts the surrounding tree without changing orientation,
and independently reconstructs the expected host root. Wrap-first uses shape 0
leaves; direct mode keeps IxVM leaves raw. Both policies then select their flat
or structural heterogeneous shapes from the production plan. Measured runs
require q=100, jobs=1, --no-cache, and JSON output; the harness natively
verifies all four inputs and every recursive output, persists every completed
slot as an ordinary Ixon.Proof, and writes resumable per-slot and whole-run
timing/RSS/proof metadata.

The existing bench-typecheck --recursive --join row keeps its stable
join-* reporting/dashboard schema, but its join phase now exercises direct
ix_aggr shape 2 instead of the retired join_two circuit. Historical size
baselines remain recorded: 7,986,166 bytes (7,986,204 with the Ixon.Proof
wrapper) for a one-constant q=100 lift under the pre-E2 247-function system,
versus 7,443,023 bytes (7,443,061-byte wrapper) after WP-E2, a 543,143-byte /
6.80% reduction. Pruned multiproof length depends on sampled query-path
overlap, so a positive-PoW output is now a sample rather than an exact
byte-length pin. The deterministic q=100/PoW-0 regression signal is 7,447,279
bytes and 38,850,942,825 recursive-verifier FFT cost. The target-box policy
comparison is the remaining measurement, not remaining harness work.

Test coverage

The legacy aggregate-first suite uses a small stand-in child system so it can
create real Multi-STARK child proofs without the tens to hundreds of GiB
required by a full recursion proof. Those proofs are consumed by the retained
join circuits, so proof validation, statement decoding, set/path folding,
native advice, and transitive recursion are exercised end-to-end at execution
time.

The 57 passing legacy aggregate-first cases cover:

  • canonical flat and structural host folds; manifest parsing, validation, and
    post-order lowering;
  • distinct lift/flat/structural compiled entrypoint identities;
  • honest flat union and cross-child discharge; structural path discharge plus
    a carried assumption;
  • flat-join and structural-join outer claim layout and native verification;
  • transitively pinned flat and structural children; rejection of a child
    carrying a different allowed digest;
  • generated/interpreter parity for both join modes; threshold scheduling that
    is flat below and structural above monotonically;
  • strict keyed-blob framing; wrong-root and tampered-sibling paths; a missing
    path choice; a carried candidate omitted from the output; the obsolete
    88-byte allowed blob; a flat join fed a genuinely free-form structural
    child root; omitted/extra assumptions or subjects; noncanonical unsorted
    trees; tampered child proofs;
  • a synthetic three-leaf manifest with empty outer leaves: raw coverage before
    pruning, contraction, dense remapping, and singleton-lift classification;
  • claim-only shard preparation preserving exact subject/assumption trees,
    with the one-pass witness closure matching the legacy per-owned-root union;
  • exact legacy verifier reconstruction of a lift outer claim and native
    verification under it;
  • byte-for-byte equality of the shared default recursion builder with the
    former direct construction; the exact 40-byte FRI cache encoding;
    independent recursion-vk identity changes for FRI and commitment overrides;
  • cache-key invalidation across version, recursion vk, FRI parameters, and
    outer claim; whole-plan statement/claim/key derivation before proving;
    atomic index creation, corrupt-index detection and repair; a
    content-addressed, claim-bound, natively verified resume hit; mismatched
    claim rejection; corrupt store-content fallback; checked rejection of
    malformed proof bytes without a Rust panic;
  • heaviest-first admission with deterministic tie-breaking; job and byte
    ceilings; dependency release; admit-alone oversize handling; the flat-join
    affine weight and MemTotal parser; jobs=1/jobs=2 payload equality;
    failure draining without admitting a dependent join; jobs=1/jobs=2
    byte-identical recursive wrappers under canonical zero-PoW grinding, with
    each scheduled child decoded and verified; and
  • Rust writer normalization plus round-tripping a genuinely noncanonical
    structural CheckEnv root.

The wrapper-byte scheduler gate uses zero PoW deliberately: with the pinned
positive-PoW implementation, rayon's find_any selects any passing grind
witness, so two valid serial proofs already differ in bytes. Production
jobs=1/jobs=2 correctness is the stronger protocol invariant — exact claims
and native verification under the same recursion system. The primitive
Multi-STARK and recursive-verifier suites were rerun to guard the shared
verifier and lift paths, and the separate activation gate covers 132 accepted
shape/input cases twice.

The focused ix-aggr command now runs 93 passing checks. Its 42
circuit/shape/plan checks cover both wraps, all flat and structural child-kind
pairs, a nested structural self child, generated/interpreter parity, strict
tree and native keyed-blob framing, identity/shape closure, wrong or tampered
paths, omitted carries, unsorted trees, flat↔structural hint confusion, a flat
parent fed a free-form structural root, and exact four-shard wrap-first/direct
plans. Its 51 converged semantic checks cover uniform claims, cache v2 key
invalidation and verified resume, recursion parameters, manifest
validation/pruning, singleton/canonical/hybrid value reconstruction,
shard-preparation closure sharing, wrap-first/direct planning, per-shape RAM
admission, dependency/failure behavior, and jobs=1/jobs=2 determinism including
concurrent zero-PoW proving. Rust unit tests separately pin channel-6 key layout
and strict path-blob framing.

Validation run

All of the following pass on the current stack:

nix develop --command lake exe IxTests aggregate-first # 57/57nix develop --command lake exe IxTests ix-aggr # 93/93nix develop --command lake exe IxTests aggregate-activation # 132 cases × 2 stable passesnix develop --command lake exe IxTests multi-stark # 12/12nix develop --command lake exe IxTests recursive-verifier # 6/6nix develop --command lake build IxTests bench-typecheck bench-recursion-debug bench-aggregate-policy ixnix develop --command cargo test -p ixvm-codegen aiur_multi_stark_runnernix develop --command cargo test -p ixvm-codegen aiur_ix_aggr_runnernix develop --command cargo test -p ix-kernel shard::tests # 23/23nix develop --command cargo test -p ixon test_check_env_claim_accepts_structural_rootnix develop --command cargo clippy -p ixvm-codegen -p ix-ffi -p ix-kernel -p ixon --tests -- -D warningsnix develop --command cargo fmt --all -- --checknix develop --command lake exe ix codegen --checkgit diff --check

Both benchmark entrypoints also reject --queries 0 with a usage error before
building or proving, and a stored pre-E2 proof now reports a checked decode
error rather than aborting. The fresh post-E2 aggregate fixture verifies
against both its .ixe and .ixes inputs.

A q=1 live smoke of the converged typecheck join also executed, proved, and
verified direct shape 2 successfully: 1.221742 s execution, 26,738,665,748 FFT
cost, 52.380108 s proving, 21,565,714,432-byte peak RSS, 401,340-byte proof,
and 0.002616 s verification. The M1-f policy harness passed plan-only smoke for
both policies and emitted the expected resumable JSON schema.

Pre-convergence real-environment gates on the 124 GB development host (kept
as WP-A/WP-B baselines for the M1-f rerun):

  • A two-shard manifest from the 5,986-constant tc-parity.ixe environment
    passes coverage and structural scheduling with threshold zero
    (2 lifts + 1 binary joins (1 structural)), and its one-shard variant
    plans 1 lifts + 0 binary joins.
  • Before E2, a one-constant q=100 production lift ran end-to-end through the
    cache and scheduler, establishing WP-A's invalidation/repair baseline:
PassResultWall
Initialproved and cached root 090bea6f…ca1535~64 s
Identical rerunverified cache hit, same root1.66 s
Truncated indexclean miss, re-proof, atomic repair, same root~38 s
Post-repair rerunverified cache hit, same root1.79 s
--jobs 2 --max-ram 400195 GiB reserved, verified hit, same root1.45 s

At scale, the measured pre-E2 4-shard Init run in Measured performance
above exercised the former backend with real shard proofs, four q=100 lifts,
two lower joins, and a root join with genuine assumption discharge. The pinned
M1-f harness will repeat this fixture under both converged policies.

Soundness summary

  • Every present child is fully verified under the verifying key selected by
    its shape-constrained kind.
  • Child function indices are constrained to IxVM verify_claim or ix_aggr,
    and every recursive child must carry the same allowed digest, pinning the
    two verifying keys and two entrypoints transitively.
  • Claims and allowed data are Blake3-bound and strictly parsed with no trailing
    bytes.
  • Flat mode reopens canonical roots and proves exact union/difference;
    structural mode proves the exact root-of-roots relation and accounts for
    every unique input assumption through either inclusion or explicit carry.
  • A bogus path, dropped candidate, extra survivor, malformed choice, or
    noncanonical assumption tree rejects. Over-carrying and duplicate subject
    leaves can only weaken the output claim; the CLI requires an unconditional
    final root.
  • Coverage is checked on the unmodified manifest, so pruning cannot conceal a
    hole or overlap; only leaves with an environment-derived owned-constant
    count of zero are removed, and contraction preserves the order and shape of
    retained leaves.
  • A singleton root is a verified shape-0 wrapper, never a raw-proof shortcut;
    the circuit binds its output digest directly to the pinned IxVM child's
    CheckEnv digest.
  • Manifest-aware verification independently repeats pruning, scheduling, and
    value folding, then verifies the one derived ix_aggr outer claim. No
    claimed proof kind or shape is accepted from wrapper metadata.
  • Proving and verification build the recursion system from the same explicit
    parameter object; any parameter change necessarily changes the recursion vk,
    allowed-system identity, and cache key.
  • Cache keys bind version, recursion-vk digest, explicit FRI bytes, and exact
    outer claim, all derived before proving; hits re-hash store content, bind
    the claim, decode without panicking, and verify natively. Corrupt, missing,
    or stale entries are misses, never trusted successes or command failures.
  • A pair is admitted only after both child slots complete; immutable
    snapshots and indexed installation make arrival order irrelevant. The
    controller owns all reservations; failures drain running work before exit.
  • Free-form structural roots are serialized as opaque commitments
    (crates/kernel/src/claim.rs); canonical openings are required only by
    protocol paths that explicitly need them.

Review map

Commit-by-commit review is recommended — each commit is feature-scoped with
its own tests (map in the first comment / commit list).

AreaPrimary filesWhat to review
Legacy regression protocolIx/MultiStark/Aggregate.lean, Ix/MultiStark.leanpre-convergence 96-byte identity and semantic/audit reference surface
Production circuitIx/Aggr/Circuit.lean, Ix/Aggr.lean80-byte identity, uniform claims, shapes 0–9, heterogeneous child pinning, structural path checks
Converged host modelIx/Aggr/Host.leanfree-form structural roots, canonical survivors, path extraction
Host statement modelIx/MultiStark/Host.leanfree-form subject roots, canonical assumptions, path extraction
Manifest parsing + pruningIx/Cli/CheckCmd.leanstrict tree parsing, validate-before-prune, unary contraction, dense remap
Aggregate driverIx/Cli/AggregateCmd.leanwrap-first/direct shape policy, singleton wrap, uniform slot claims, cache v2, per-shape RAM weights, verified DAG resume/failure draining
Root verificationIx/Cli/VerifyCmd.leanone ix_aggr outer claim, value-based environment comparison, manifest-relative hybrid reconstruction
Recursion parametersIx/MultiStark.lean, Ix/Aggr.lean, Ix/Cli/{Aggregate,Verify}Cmd.leanshared config plumbing, deterministic systems, stable FRI cache encoding
Pruned FRI multiproofsIx/Aiur/{Protocol,Semantics/BytecodeFfi}.lean, Ix/Cli/{Aggregate,Verify}Cmd.lean, crates/ffi/src/aiur/protocol.rscompact persisted proof vs expanded recursive advice boundary, checked legacy-proof failure
Cache/store boundaryIx/Store.lean, Ix/Aiur/Protocol.lean, crates/ffi/src/aiur/protocol.rswipeable namespace, checked proof decoding, corrupt-data fallthrough
Shard preparationIx/IxVM/ClaimHarness.lean, Ix/Cli/{Aggregate,Check,Verify}Cmd.leanclaim/tree-only consumers, one-pass multi-root witness closure
Native advicecrates/ixvm-codegen/src/aiur_multi_stark_runner.rsstrict framing and seven-channel key layout
Converged native advicecrates/ixvm-codegen/src/aiur_ix_aggr_runner.rs, crates/ffi/src/aiur/protocol.rsstrict path framing, shared channel-6 key layout, execute/prove passthrough
Generated executorcrates/ixvm-codegen/src/aiur_multi_stark.rsgenerated artifact; verify with ix codegen --check
Converged generated executorcrates/ixvm-codegen/src/aiur_ix_aggr.rsgenerated artifact; verify with ix codegen --check
Manifest writercrates/kernel/src/shard.rsproducer-side omission, tree remap, aggregate metadata
Claim semanticscrates/kernel/src/claim.rs, crates/ixon/src/proof.rsfree-form root contract and codec regression test
TestsTests/Aggr.lean, Tests/AggrSemantics.lean, Tests/AggrActivation.lean, Tests/MultiStark.lean93 production shape/semantic/plan checks, 132-case activation audit, retained Ix-agnostic regressions
BenchmarksBenchmarks/AggregatePolicy.lean, Benchmarks/Typecheck.lean, bench registry/dashboardspinned four-shard wrap-first/direct handoff, resumable JSON, converged direct-shape-2 join metrics, historical lift-size pins

Current limits and non-goals

  • Recursion FRI parameters stay at the q=100 defaults; the q=50 + compensating
    PoW policy is an explicit follow-up decision (the measured costs above are
    therefore the conservative end).
  • Structural roots are manifest-relative. Environment-only verification
    accepts only a bundled value equal to the canonical environment root;
    hybrid roots should be verified with both --ixe and --ixes and the
    proving threshold.
  • Scheduler RAM weights are calibration-pending placeholders (validated in
    the safe direction by the measured run); concurrent proves share the rayon
    global pool, so throughput can sub-scale — process-per-slot is a
    measurement-driven follow-up.
  • Same-key cross-process cache writes degrade to a warning and recomputation,
    never a soundness issue.
  • Dummy-call activation padding is deferred until/unless a static terminal
    circuit is selected; under the SP1 terminal it is pure cost.
  • Aggregate wrappers reuse Ixon.Proof and do not encode the proof system;
    callers must pass ix verify --aggregate.
  • M1-a–f land the converged production circuit, host/native substrate, driver,
    cache, scheduler, value-based verifier, semantic union, widened activation
    audit, and policy benchmark harness. The only remaining M1 work is executing
    the wrap-first/direct comparison on the target box. The pinned M1-f revision,
    inherited M1-e preflight, and both measured policies are one checkout/session;
    its handoff is documented in
    plans/aggregate-first-m1f-large-box-runbook.md.
  • This PR does not adopt cold-circuit grouping and does not implement a
    terminal KZG/SP1 wrapper.

Follow-ups

  1. Calibrate lift/join unit costs on a known-core box, with per-node
    flat-vs-structural attribution, and measure frontier/discharge-path counts
    at Mathlib scale (plan §11.4.1).
  2. Decide the recursion q=50 + PoW policy (§9.5) now that the mechanism
    exists.
  3. Connect the root proof to the selected terminal compression path (E3:
    single-recursion-proof SP1 guest + ix compress-root).
  4. Re-evaluate cold-circuit grouping (E4); E2's measured outer-lift reduction
    was only 6.80%, so grouping may still be material.
  5. Process-per-slot proving if measured rayon contention warrants it.
  6. Run the exact M1-f commit
    b70042d6d1a029bb8e1d541da7aef5e0613d67e6 for the combined M1-e preflight
    and wrap-first/direct policy benchmark session in the large-box handoff
    runbook.

johnchandlerburnhamand others added 22 commits August 27, 2026 16:21
Add the deterministic lift/flat/structural activation matrix and keep dummy calls deferred. Split aggregate recursion commitment/FRI configuration from IxVM defaults, share it between proving and verification, and pin the future cache encoding without changing active protocol parameters.
Precompute versioned per-slot cache keys, persist lift and join wrappers in the content-addressed store, and reuse entries only after exact claim and native outer-proof verification. Add safe proof decoding, corruption recovery, and --no-cache.
Execute ready lift and join slots as a dependency DAG under explicit job and RAM admission. Add calibration-pending slot weights, failure draining, CLI controls, and serial/parallel scheduler and proof-equivalence gates.
Add an opt-in two-child benchmark that proves singleton CheckEnv shards, lifts both proofs, and measures a verified flat join. Wire join metrics through reporting and dashboards, and record the current pre-E2 lift-size baseline.
- rust-toolchain.toml channel: 1.92 → 1.98, with the matching fenix
toolchain hash in flake.nix. The pinned fenix already carries the 1.98
release manifest, so flake.lock needs no change (fenix's nixpkgs stays
pinned via its lean4-nix follows, so the Lean toolchain is untouched).
- Drop clippy::from_iter_instead_of_collect from the workspace lints:
removed in clippy 1.98 and now warns as unknown.
- Fix the warnings new clippy 1.98 lints surface across the workspace:
chunks_exact(N) → as_chunks::<N>() where the chunk size is constant,
descending sort_by → sort_by_key(Reverse(..)), iteration over map
values via .values(), map().unwrap_or() → map_or(), a checked
division, an unwrap-after-is_some restructured into if-let, and
assorted redundant-reference/pattern cleanups (mostly cargo clippy
--fix). The two byte-gadget files keep their chunks_exact warnings
until the next commit, which rewrites those regions anyway.
CI derives its Rust version from rust-toolchain.toml, so no workflow
changes are needed.
Companion to multi-stark's update-p3 branch (c72d321 → 249b740), which
carries four soundness/robustness fixes and the Plonky3 v0.6.0 bump
(pruned FRI Merkle multiproofs: ~2x faster verification, 40-70% smaller
proofs; canonical Goldilocks serde removes proof-byte malleability).
Proofs and verifying keys are not compatible with the previous pin.
Integration:
- Lookup gained max_multiplicity, a declared per-row bound on the
multiplicity's integer magnitude feeding the newly enforced logUp
height bound Σ wᵢ·hᵢ + |claims| < p. Function-circuit slots accumulate
mutually-exclusive branch selectors, so they declare 1; committed
count columns (function return slots, the memory circuit, the byte
gadget tables) declare the new COUNT_COLUMN_BUDGET (2^32 queries per
entry).
- The VK wire format carries the bound: u64 LE max_multiplicity per
lookup, between the multiplicity node id and the arg count. The
in-circuit VK deserializer (Ix/MultiStark/SystemDeserialize.lean)
parses past it; the value is bound through the vk digest but the
height bound itself is not yet enforced in-circuit (the native
verifier enforces it).
- Message fingerprints are width-bound by default upstream (the slot
width seeds the Horner fold), which is incompatible with aiur's
branch-shared lookup slots: mutually exclusive branches superpose
messages of different natural widths into one slot at the maximum
width, so a narrow call is sent zero-padded to a width its callee's
return slot never provides, and proving fails with
UnbalancedChannel (pinned as the prove_verify_mismatched_call_widths
regression). Aiur instead declares WidthBinding::ByConstruction —
the plain Horner fold, restoring zero-padding transparency — and
takes on the prefix-freeness contract that makes it sound: every
message's natural width is a function of its constant-constrained
leading prefix (channel tag plus discriminator: fun_idx fixes
2+in+out, the memory size coordinate fixes 3+size, each gadget tag
fixes its table width), so zero-extension can only equate a padded
message with its own natural form. The contract is documented at the
channel constants in lib.rs; the declaration is applied in
AiurSystem::build and mirrored in the vk_codec decoder so decoded
VKs replay the same transcript. ByConstruction is also exactly the
fold the in-circuit verifier's logup_fingerprint already computes,
so the recursive verifier needs no fingerprint change.
- The policy is Fiat-Shamir-bound as the first observe_shape word; the
in-circuit transcript replay prepends the matching limb.
- aiur_multi_stark.rs regenerated (ix codegen) for the deserializer
and transcript changes.
- P3 v0.6.0 ships FRI query openings as pruned Merkle multiproofs,
while the in-circuit verifier consumes one authentication path per
query (its per-query control flow is a far smaller circuit than the
amortized multiproof walk). Rather than porting the walk into the
DSL, the proof advice stays in the per-query transport: multi-stark's
new advice module re-encodes a natively-verified proof by running
p3's own verification with a recording compression function and
reading each query's path back out of the recorded digest map. The
advice bytes are untrusted verifier input, never digest-bound —
the transcript binds the commitments and every expanded sibling is
authenticated against them per query — so pruning vs expansion is
pure transport and the encoding choice is sound. AiurSystem gains
proof_to_advice_bytes (FFI: AiurSystem.proofToAdviceBytes); the
recursive-verifier test feeds it instead of Proof.toBytes, whose
native wire format is still round-tripped separately. The Lean-side
proof grammar and the codegen'd verifier are byte-identical to
before — no in-circuit changes.
Claim layout, the VK wire format above and aiur's public semantics are
otherwise unchanged; the policy adds no prover or verifier work over
the previous pin.
Still open, native-verifier-only: the logUp height bound is parsed
past but not yet enforced in-circuit (a wide-arithmetic check, tracked
separately).
The Lean v4.33.1 update pinned both dependencies at revisions that
predate their Rust 1.98 bumps; their heads now carry those bumps, which
this workspace needs since rust-toolchain.toml moved to 1.98. Both
revisions stay on leanprover/lean4:v4.33.1.
- Blake3.lean 1b0fbd2 → e6e908b (Rust 1.98, plus a case-insensitive
source-directory fix), updated in lakefile.lean, lake-manifest.json
and the blake3-lean flake input. The revision keeps the
`blake3_rs_shared` target the `ix_native_decide_dynlib` pin requires.
The inherited entry in Benchmarks/Compile/lake-manifest.json was still
on the pre-4.33.1 revision and now tracks the root pin.
- lean-ffi 2a9c91e → 93c7e52 (Rust 1.98). Only bignat reaches the sp1
and zisk workspaces, so their lock files move that one package.
The !benchmark recursive phase reported n/a for every fri-verifier
metric: Benchmarks/Typecheck.lean still fed Proof.toBytes — the pruned
multiproof wire format — to executeMultiStark/proveMultiStark, so the
in-circuit verifier rejected on parse and the harness (correctly) left
the recursive fields absent rather than emit a fake datum. The
in-circuit verifier consumes the per-query advice transport
(AiurSystem.proofToAdviceBytes); proofBytes stays the reported
proof-size metric.
bench-recursion-debug had the same advice-format gap plus a stale
claim recipe: it still built the public input as 32 raw digest bytes,
predating the ClaimHarness.packedDigestKey packing bench-typecheck
uses (its own out-of-circuit sanity check failed with
InvalidPowWitness — a wrong claim diverges every challenge — and the
advice re-encoder refused the proof for the same reason). Both aligned
with the typecheck flow.
Validated end-to-end at production parameters (numQueries 100,
query PoW 20, blowup 2) on Nat.add_comm: inner prove, advice
re-encoding, and the codegen'd in-circuit verifier accepting.
Keep compact proof bytes at storage and cache boundaries, expand them only for recursive lift/join advice, reject unsupported zero-query benchmarks, and decode legacy store proofs without panicking. Refresh tests, benchmark pins, and Rust 1.98 lint compatibility.
Add Ix/Aggr, a recursive aggregation system for IxVM shard proofs that
keeps Ix/MultiStark untouched and Ix-agnostic. One entrypoint, ix_aggr,
subsumes lifting and joining: a one-byte advice shape selects wrap or
binary join over any mix of IxVM and ix_aggr children, so shard proofs
enter the recursion system directly as join children and the dedicated
lift stage disappears.
Circuit (Ix/Aggr/Circuit.lean): every shape verifies its children in
full (verify + ood_verify from the shared Multi-STARK verifier modules)
against the vk its hinted kind demands. Identity is one 80-byte
digest-bound blob - blake3(ixvm vk) || verify_claim idx ||
blake3(self vk) || ix_aggr idx - carried unchanged at every node; self
children must bind the identical blob digest, pinning both vks and both
entrypoint indices transitively. Claims are a uniform 18-word
[0, aggr_idx, allowed(8), checkEnv(8)] at every depth, so proofs of
different tree levels combine freely. Wrap shapes bind the output
digest to the child CheckEnv digest directly; pair shapes open both
CheckEnv preimages, re-root the canonical subject/assumption trees, and
prove subjects = L ∪ R, assumptions = (asmL ∪ asmR) ∖ subjects with
linear sorted merges.
Toplevel (Ix/Aggr.lean): ixAggr = MultiStark.multiStarkFull + circuit,
pruned to ix_aggr, so verify_multi_stark_proof and other unrelated
entries no longer pad aggregate proofs. The host half of the wire
contracts (allowed blob, public input packing, shape codes, keyed
preimage/tree blob framing, interpreter IO assembly) lives beside the
toplevel; Ix/Aggr/Host.lean folds CheckEnvTrees statements.
Native path: ix codegen gains the ix-aggr target
(crates/ixvm-codegen/src/aiur_ix_aggr.rs, 244 fns); its runner builds
the seven-channel IO buffer natively and routes execution through the
generated code. New FFI rs_aiur_ix_aggr_execute/_prove
(executeIxAggr/proveIxAggr) pass proofs, vks, claims, and the compact
count/key/length preimage/tree blobs without per-byte boxing, plus
Proof.ofBytesChecked for store-boundary decoding.
CLI: ix aggr --ixe E --ixes M <shard-proof>... reconstructs every
nonempty shard statement from the env, matches wrappers by claim
digest, natively pre-verifies them, folds a balanced bisection (the
canonical fold makes the root claim independent of tree shape), wraps
single-shard roots so the persisted root is always an ix_aggr proof,
checks the root closes over the env canonical tree with no residual
assumptions, and persists the wrapper.
Tests (lake test -- ix-aggr, 17 cases): all five shapes accept over
real Multi-STARK stand-in child proofs from two distinct-vk systems;
codegen'd execution matches the interpreter on output and per-circuit
query counts for wrap and pair; negatives break one binding each -
lying shape hint, tampered proof, foreign identity, wrap statement
drift, dropped assumption, padded subject set, and tree advice not
reproducing its keyed root.
Replace the legacy lift/join entrypoints with the heterogeneous ix_aggr system across aggregate, verification, cache, and codegen paths. Preserve wrap-first and direct-join policies behind one proof identity and cache namespace.
Add the converged 91-check semantic suite and a deterministic 132-case activation audit covering shapes 0 through 9 twice, with no unobserved circuits.
Replace the legacy two-shard three-entrypoint benchmark with a four-shard manifest-subtree harness for production ix_aggr wrap-first and direct policies. Enforce the q=100 serialized no-cache profile, natively verify every input and recursive output, persist ordinary aggregate wrappers, and emit resumable per-slot JSON metrics.
Port bench-typecheck --join to ix_aggr direct shape 2 while preserving the stable join metric schema. Remove the final legacy slot-spec/preimage shim and pin both M1-f four-shard plans in the focused suite.
Update the multi-stark dependency and Rust toolchain for Plonky3 0.6, along with the Rust 1.98 lint migrations required to keep the workspace warning-free. Refresh the Rust-compatible Blake3.lean pin in both root and compile-package manifests.
Adapt recursive Aiur verification to Plonky3's pruned FRI multiproofs. Native proofs retain their compact serialized representation and native verification path; the FFI expands authenticated Merkle frontiers into per-query advice only when entering the existing recursive verifier circuit.
Preserve the packed claim-digest convention in the recursion diagnostic and exercise the advice boundary in the end-to-end test and benchmark paths. CPU and CUDA recursive q1 runs produce identical 823,485-byte inner proofs and 331,273-byte outer proofs.
The q50 Vector.extract_append workload retains identical CPU/CUDA proof sizes. Inner plus outer STARK proving measures 65.87s on CPU and 8.81s with CUDA on the RTX PRO 6000, a 7.48x speedup.
PR benchmark runs execute trusted workflow YAML from the default branch while loading composite actions from the PR checkout. When Bencher data and binary caches are unavailable, the workflow checks out main under base/ and asks Lake to rebuild it without first installing the Rust channel pinned by that checkout.
Teach the existing CPU provenance action to install the base checkout's validated Rust channel and profile immediately before an uncached base build. The step is a no-op when the toolchain is already available and leaves cached benchmark comparisons unchanged.
Consume the Plonky3 0.6 batch-opening layout directly in Aiur instead of expanding every pruned Merkle frontier into one authentication path per FRI query. Sample all query indices from the unchanged transcript, sort and deduplicate them with an O(q log q) merge sort, authenticate each input and commit-phase commitment once, then retain the existing per-query reduced-opening and FRI arithmetic.
Bind every frontier to transcript-derived indices, consume boundary digests in Plonky3's level/parent/child order, reject trailing frontier elements and inconsistent duplicate leaves, and assert all native opening dimensions and sibling counts. Explicitly constrain the digest-bound protocol specialization to cap height 0, binary FRI, and a constant final polynomial. Move memo_u32_less_than into IxVM Core so both substitution and multiproof sorting share its constrained rows.
Strengthen the recursive negative test to mutate a structurally valid stage-1 commitment. Regenerate both checked-in Aiur Rust executors and retain interpreter/codegen query-count parity.
On Vector.extract_append q50, recursive-verifier FFT cost falls from 204.073B to 201.166B. CPU outer proving improves from 50.09s to 45.03s and the full CPU pipeline from 90.64s to 82.90s. GPU outer proving improves from 15.85s to 13.72s and the full GPU pipeline from 28.86s to 26.69s. The outer proof grows from 3.92 MB to 4.17 MB.
Validated with the MultiStark primitive suite, recursive honest/tamper/parity tests, codegen --check, release workspace clippy, release CUDA clippy, rustfmt, and diff checks.
@johnchandlerburnham

Copy link
Copy Markdown
MemberAuthor

Init full proving report

Revision: 82dcc9dba36d0cfd6c753c6878ab649643a52026
Host: 64 vCPU, 495 GiB RAM, 256 GiB swap
Final result: one verified ix_aggr FRI proof with no remaining assumptions.

Compilation and sharding

StepResult
Compile sourceBenchmarks/Compile/CompileInit.lean
Ix compiler time1.876s
Constants compiled65,994
init.ixe size194,296,853 bytes (185.3 MiB)
Static sharding time3.1s
Manifest16 nonempty shards
Retained/owned constants56,621
Manifest size2,619,404 bytes (2.50 MiB)
Predicted FFT per shard4.160e11–4.197e11
Predicted FFT spread1.01×

The 1.876-second compile measurement is the Ix compiler itself; it excludes the initial one-time dependency download and Lake build.

Stage 1: base shard proofs

All 16 shard proofs were persisted and individually verified against their corresponding manifest claims.

MetricSerial baselineTwo-way run used for aggregation
Concurrency12
End-to-end wall time24m 16.76s15m 53.93s
Sum of shard prover times22m 43.23s30m 08.14s
Per-shard time range74.08–113.97s98.94–142.29s
Median shard time82.66s108.75s
Highest individual RSS219.8 GiB219.8 GiB
Combined process-tree peak358.2 GiB
Proof count1616
Total proof-wrapper size369,995,038 bytes
Mean proof-wrapper size23,124,690 bytes
Proof-wrapper range22,714,520–23,998,692 bytes

Two-way concurrency gave a 1.53× wall-time speedup despite increasing total prover-seconds by about 33% from CPU/memory contention.

Stage 2: recursive aggregation

Policy:

  • Wrap-first
  • --structural-above 4096
  • --jobs 2
  • --max-ram 450
  • Default q=100 recursion parameters
  • Aggregate cache enabled

Plan:

OperationCount
Shard wraps16
Binary joins15
Structural joins14
Flat joins1
Total recursive slots31

Results:

MetricResult
Wall time45m 06.61s
Peak resident memory379.9 GiB
Average CPU utilization4,192% (~42 cores)
Cache hits0
Freshly proved/cached slots31
Swap used0 bytes
Exit status0
Final proof-wrapper size9,596,044 bytes (9.15 MiB)

Final proof

Root address:

80f6844f968920abaef8d8c750a1a1dc1ebe32657a3cfb824e1810bcd65c8814

Final claim:

CheckEnv(6dc8f821a989c6f7b9d520b053ef8edbd582d2408d7b6a6ff72b908666e03b6f, none)

Both validations passed:

  • Cryptographic-only aggregate verification
  • Manifest-aware verification against init.ixe, init-16.ixes, and structural threshold 4096

End-to-end measured time

Using the two-way Stage 1 run:

ComponentTime
Ix compilation1.876s
Sharding3.1s
Stage 1 proofs15m 53.932s
Stage 2 aggregation45m 06.61s
Measured core pipeline1h 01m 05.52s

This total excludes the one-time dependency build/download and a few seconds of manifest and final-root verification overhead. Using serial Stage 1 instead would make the measured core pipeline approximately 1h 09m 28.35s.

Use the PR77 lean-ffi and multi-stark pins, remove the stale width-binding and multiplicity-bound protocol fields, and regenerate the recursive and aggregate verifier sources.
Expose an opt-in x86_64-linux cuda shell with NVCC 13.2, sm_120 code generation, and safe host libcuda discovery while leaving the default development shell unchanged.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@johnchandlerburnham@samuelburnham@arthurpaulino
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root - #598

Open
johnchandlerburnham wants to merge 24 commits into
mainfrom
jcb/aggregate-first
Open

Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root#598
johnchandlerburnham wants to merge 24 commits into
mainfrom
jcb/aggregate-first

Conversation

@johnchandlerburnham

@johnchandlerburnhamjohnchandlerburnham commented Aug 27, 2026

Copy link
Copy Markdown
Member

Summary

This PR implements the aggregate-first path for Aiur Multi-STARK proofs. The
production path now runs through one ix_aggr recursion entrypoint: it wraps
independently-proven IxVM shards, folds their CheckEnv statements along the
manifest's bisection tree, and persists one recursive root proof with a
uniform 18-word outer claim. Every completed recursive slot is persisted in a
verified, content-addressed version-2 resume cache, and ready slots are proven
in parallel under explicit job and RAM limits.

The entrypoint has two pair-fold modes:

  • Flat shapes perform a canonical set union/difference and are efficient for
    small lower nodes.
  • Structural shapes commit to nodeHash(leftRoot, rightRoot) in one hash and
    discharge assumptions with Merkle inclusion paths. Their 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 (default
4096 subject leaves): flat below the threshold, structural above it.

IxVM shard proofs
│
├── ix_aggr shape 0 (default wrap-first policy)
│ or remain raw with --direct-joins
▼
ix_aggr proofs carrying uniform CheckEnv statements
│
├── flat ix_aggr shapes 2–5 below the configured subject threshold
│ subjects := canonical(subjectsL ∪ subjectsR)
│ assumptions := (assumptionsL ∪ assumptionsR) ∖ subjects
│
└── structural ix_aggr shapes 6–9 above the threshold
subjects := nodeHash(subjectRootL, subjectRootR)
assumptions := candidates carried or discharged by inclusion path
▼
Ixon.Proof {
claim := CheckEnv(manifest-relative subject root, none),
proof := one recursive Multi-STARK root proof
}

A manifest with one retained (nonempty) shard is also a valid deliverable: its
root is one shape-0 wrapper, with no self-join padding (including under
--direct-joins). Zero-constant manifest leaves are pruned after raw coverage
validation:

raw manifest
|
| validate exact environment coverage before pruning
v
drop zero-constant leaves + contract unary tree nodes + remap retained ids
|
+-- one retained leaf --> wrap once and persist the ix_aggr root
|
`-- multiple leaves --> fold flat/structural joins as above

This is the stage-2 aggregation layer from
plans/aggregate-first-pipeline.md: shard proofs are aggregated before any
terminal KZG/SP1 compression, so a later wrapper only needs to consume one root
proof. Terminal compression remains outside this PR.

Section 14 implementation is complete through M1-f in this branch. The
ap/ix-aggr single-entrypoint base is reconciled with WP-E2's compact stored
proof / expanded recursive-advice boundary, and its one ix_aggr entrypoint
supports wraps, flat pairs, and structural pairs across both IxVM and recursive
children. ix aggregate now uses that backend for planning, proving, cache
resume, and final verification; ix verify --aggregate derives one uniform
outer claim and performs value-based environment/manifest checks. The old
ix aggr command and all three-entrypoint production wiring are retired. The
older Multi-STARK aggregate implementation remains as an Ix-agnostic regression
surface, not as a CLI backend. Production semantics, activation coverage, and
benchmarking now live entirely against ix_aggr. M1-f lands the pinned
four-shard bench-aggregate-policy handoff and ports the stable typecheck join
metric to direct shape 2; only execution of the policy comparison on the large
box remains. Operationally this is one future handoff: Arthur will check out
b70042d6d1a029bb8e1d541da7aef5e0613d67e6 once, run the inherited M1-e
correctness suite as untimed preflight, and then benchmark both policies. There
is no separate M1-e box run.

Measured performance (2026-08-29)

Arthur Paulino ran the pre-convergence pipeline end-to-end on four real Init
shard proofs (shards 8–11 of a 16-way partition, 12,493 constants; base shard
proving excluded; native verification of each output included; ~512 GB box).
These are the standing M1-f comparison baseline; the converged wrap-first and
direct policies have not yet been rerun on that box. The recursion used the
conservative q=100 / PoW 20 defaults this PR keeps:

StageTimePeak RAMOutput proof
Lift × 498.8–103.0 s (mean ≈ 101 s)186.8–195.8 GiB8,162,462 B each
Join 8+9, 10+1148.9 / 48.5 s~102.5 GiB9,502,843 B
Root join85.8 s156.9 GiB9,455,498 B
Total (serialized)10:23 wall195.8 GiB9,455,498 B

Notes: lift cost is shard-content-independent (four lifts within ±2%, tracking
the ~constant 20–23 MB shard-proof shape); the measured lift peak brackets the
scheduler's 195 GiB weight placeholder exactly; and an unserialized run of all
four lifts OOM'd at ~492 GiB — precisely the behavior the WP-B admission gate
prevents (on a 512 GB box its weights admit two lifts). Because shards 8–11
are a strict partition subset, the root carries a real frontier assumption
set, so the joins exercised genuine discharge work. Full analysis, including
the comparison against direct (unlifted) IxVM joins, is in
plans/aggregate-first-pipeline.md §3.4 and the Zulip thread.

What lands

Protocol / circuit layer:

  • One production recursion entrypoint, ix_aggr, under one verifying key.
    Shapes 0–1 wrap one IxVM/recursive child, shapes 2–5 fold flat pairs, and
    shapes 6–9 fold structural pairs for every child-kind combination.
  • Recursive validation of wrap, flat-pair, and structural-pair children with
    transitive verifying-key, function-index, and allowed-system binding. A
    shape hint selects the verified form but cannot weaken its checks.
  • Strict in-circuit CheckEnv decoding and canonical flat set folding.
  • Structural subject folding with per-candidate Merkle discharge paths while
    keeping output assumption sets canonical and deduplicated.
  • One 80-byte protocol identity containing the IxVM/self verifying-key
    digests and their two accepted entrypoint indices; every recursive node has
    the same 18-word outer-claim layout regardless of shape.
  • Structural outputs bind nodeHash(leftRoot, rightRoot) and account for each
    unique assumption candidate through a strict Merkle-path-or-carry choice.
  • Host helpers construct structural roots, survivors, and path advice. Channel
    6 carries both the one-byte shape at key [0] and candidate path payloads at
    raw 32-byte address keys; the key shapes are disjoint.
  • Lean/Rust execute and prove FFI accept a compact keyed path blob, whose
    framing is decoded strictly before the circuit independently validates
    payload semantics.
  • The generated ix_aggr executor is regenerated at 2,061,361 bytes / 248
    Aiur functions and parity-tested against the interpreter.

Host driver and operations:

  • Strict .ixes aggregation-tree parsing and post-order lowering, with a
    balanced fallback for legacy manifests; validate-then-prune removal of
    zero-constant leaves with unary tree contraction (and a Rust writer that
    stops emitting them).
  • Single-shard roots: a one-retained-shard manifest always produces one
    shape-0 ix_aggr wrapper, including under --direct-joins.
  • A monotone leaf-count scheduler controlled by --structural-above, with a
    default wrap-first plan (shapes 0 then 5/9) and an explicit
    --direct-joins plan that derives heterogeneous shapes from actual child
    kinds.
  • Versioned, content-addressed aggregate cache and resume (WP-A): version 2
    binds the uniform outer claim, and reuse occurs only after content-digest,
    claim, decode, and native verification all pass; --no-cache bypasses.
  • Parallel execution of ready wraps/pairs as a dependency DAG under --jobs
    and --max-ram (default 92% of MemTotal), heaviest-first, with failure
    draining (WP-B). Admission weights are shape-aware: 4 GiB for a retained
    raw IxVM leaf, 195 GiB for wraps/self pairs, 390 GiB for IxVM/IxVM pairs,
    and 340 GiB for mixed pairs.
  • A recursion-parameter split (WP-E1): aggregate proving/verification share an
    explicit RecursionParameters value, defaulting to today's q=100 values;
    the q=50+PoW policy decision is deliberately deferred.
  • A deterministic 132-case production activation audit across shapes 0–9:
    two controlled child trace heights, both wraps, every flat/structural
    child-kind pair, optional assumptions per side, and discharge/carry. The
    complete matrix runs twice with a stable signature and observes every
    catalogued circuit; dummy-call padding remains intentionally deferred.
  • Claim-only shard preparation (no discarded dependency-byte closure walks)
    with a one-pass multi-root witness closure for proving.
  • ix aggregate --ixe E --ixes M [--structural-above N] [--direct-joins] [--jobs N] [--max-ram G] [--no-cache] [--plan-only] <proof>... and
    single-entrypoint aggregate verification with cryptographic-only,
    environment-bound, and manifest-bound value checks. The duplicate
    ix aggr command is removed.
  • Opt-in aggregate benchmarks: a tiny two-shard Init pair end-to-end bench and
    a flat-join bench (singleton CheckEnv shards → two lifts → verified flat
    join), with join metrics wired into reporting/dashboards, the pre-E2 lift
    proof size pinned at 7,986,166 bytes, and the post-E2 delta measured.
  • Positive, transitive-recursion, scheduler, cache, pruning, parity,
    malformed-input, and semantic-tampering tests: the aggregate-first suite
    has 58 cases, plus the separate activation matrix.

Protocol design

One recursion system, one entrypoint

Production builds Aggr.ixAggr, the shared Multi-STARK verifier closure pruned
to one ix_aggr entrypoint. A one-byte advice hint selects the exact verified
shape:

ShapeChildrenFold
0IxVMpass-through wrap
1ix_aggrpass-through wrap
2–5IxVM/ix_aggr pair (2 + 2·left + right)canonical flat fold
6–9IxVM/ix_aggr pair (6 + 2·left + right)structural fold

The hint is advice, not authority: each arm verifies the child proof(s) under
the key required by the encoded child kinds, then requires that kind's exact
claim layout. A wrong shape therefore fails proof verification, function-index
binding, or claim decoding.

Uniform public statement and pinned protocol identity

Every wrap and pair exposes the same public-input shape:

ix_aggr(allowed_digest: [G; 8], out_claim_digest: [G; 8])

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:

  1. the 80-byte allowed-system blob; and
  2. the serialized output CheckEnv claim.

The allowed blob is:

blake3(ixvm_vk)
|| verify_claim_idx:u64le
|| blake3(ix_aggr_vk)
|| ix_aggr_idx:u64le

The circuit requires exactly 80 bytes. 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
another function in those systems; binding the two accepted 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.
Every persisted aggregate outer claim is consequently the same 18-word form:
the ix_aggr function selector followed by the packed identity and output
claim digests. Shapes are witness choices and do not become root-kind metadata.

Child-proof validation and transitive pinning

Each pair reads the verifying key required by each child kind, checks its
Blake3 digest against the allowed blob, and deserializes it strictly. Wraps do
the same for their one child. For every child the selected arm:

  1. reads and fully consumes one proof and exactly one outer claim;
  2. runs the Multi-STARK structural verifier;
  3. runs ood_verify, including Fiat-Shamir replay, lookup accumulators,
    quotient/OOD checks, Merkle openings, and FRI verification; and
  4. decodes the statement according to the constrained child kind and function
    index.

The accepted forms are:

  • IxVM child: its system digest must equal blake3(ixvm_vk). Its claims
    digest opens to exactly one 10-word IxVM claim at the pinned
    verify_claim_idx, whose digest opens to a strict serialized CheckEnv
    claim.
  • ix_aggr child: its 18-word claim must use the pinned ix_aggr_idx; its
    first public digest must equal the current allowed_digest, transitively
    pinning both systems and both entrypoints; and its second digest opens to its
    output CheckEnv claim.

Any other index, shape, digest, trailing byte, or failed proof check rejects.
This keeps wrap→pair, flat→pair, and structural→structural composition on one
statement format.

Flat canonical folding

For child statements (S_L, A_L) and (S_R, A_R), flat shapes 2–5 enforce:

S_out = S_L ∪ S_R
A_out = (A_L ∪ A_R) ∖ S_out

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 u32 words using the
full-domain u32_less_than primitive. The implementation never uses Aiur
pointer 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

Structural shapes 6–9 perform the same allowed-blob binding, child-key
binding, two child-proof verifications, child decoding, and output-claim
binding as the flat shapes. They replace all subject-tree loading and set
re-rooting with:

S_out.root = nodeHash(S_L.root, S_R.root)

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:

  • carried (0): no bytes may follow, and the candidate must be the next
    value in the output assumption list; or
  • discharged (1): a bounded Merkle path from
    leafHash(candidate) to S_out.root must verify.

Path payloads are strict:

0
1 || count:u8 || repeated(side:u8 || sibling[32])

count is limited to 64. side = 0 hashes nodeHash(sibling, current) and
side = 1 hashes nodeHash(current, sibling); any other side rejects. The
payload 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

All ix_aggr shapes use the same seven-channel IO layout. Digest/root bindings
are checked before the corresponding bytes are decoded.

ChannelKeyValue
0[0], [1]first and optional second child proof advice bytes
1[kind]verifying-key bytes (0 = IxVM, 1 = ix_aggr)
2[0], [1], [2]left/right outer claims and output CheckEnv claim
3[0]80-byte allowed-system blob
4packed Blake3 digestnested claim preimages
5raw 32-byte addressserialized canonical trees keyed by root
6[0] / raw candidate addressshape byte / carried-or-discharged path choice

Lean passes channels 4–6 across FFI as compact framed blobs:

count:u32le || repeated(key[32] || payload_len:u32le || payload)

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.

Wraps receive no tree or path advice. Flat pairs receive full
subject/assumption tree advice and an empty path blob. Structural pairs receive
only the input/output assumption trees plus one path choice for every
deduplicated candidate; subject trees are never opened.

Manifest and host pipeline

.ixes aggregation tree

parseIxesManifest exposes both shard block lists and the optional binary
aggregation tree stored at the manifest tail. It:

  • requires nonzero, contiguous shard IDs;
  • validates option and tree tags;
  • requires tree leaves to be exactly the manifest shard set;
  • rejects duplicate/out-of-range leaves and trailing bytes; and
  • synthesizes a balanced ascending-ID tree for legacy manifests without a
    tree tail or with an explicit absent-tree tag.

The tree lowers to post-order FoldOp slots. Every join refers only to earlier
slots and the last slot is the root, providing a direct serial schedule and the
basis for cache keys and parallel execution.

Validate first, then prune empty manifest leaves

IxesManifestView retains the original shard id for every dense shard slot.
pruneEmpty:

  1. counts the environment constants owned by every raw shard;
  2. drops only shards with a count of zero;
  3. contracts aggregation-tree nodes with a removed child;
  4. densely remaps retained leaf indices; and
  5. returns the retained per-shard subject counts with the pruned view.

The aggregate and manifest-aware verifier paths deliberately call
shardsCover on the raw manifest before this transformation. Coverage
still establishes that every environment constant is owned exactly once. The
subsequent zero count is therefore evidence that a removed leaf contributes no
subject; pruning is not allowed to hide a missing or duplicated constant.

Original shard ids are preserved separately from dense scheduling ids, so CLI
plans and proof-binding errors continue to identify the source manifest shard.
An all-empty manifest is rejected because it cannot produce a root.

For a tree node node(left, right), pruning follows:

both retained -> node(left', right')
only left retained -> left'
only right retained -> right'
neither retained -> removed

The Rust .ixes writer (crates/kernel/src/shard.rs) also normalizes new
manifests before serialization — block-empty shard records are omitted, ids
rewritten densely, the tree pruned/contracted/remapped, and cross-ingress
recomputed. The Lean-side pass remains necessary for legacy manifests and for
the stronger environment-relative condition (a shard may contain blocks yet
own zero constants).

Monotone structural scheduling

The host counts actual owned constants for each shard after the disjoint-cover
check. schedulePlan annotates every slot with its cumulative subject count and
chooses a structural pair exactly when:

left.subjectCount + right.subjectCount > structuralAbove

The default threshold is 4096; --structural-above 0 makes every join
structural 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.

By default, every leaf is an ix_aggr shape-0 wrap, so binary slots are shape
5 below the threshold and shape 9 above it. With --direct-joins, a
multi-shard plan keeps leaves as raw IxVM children and derives each pair shape
from its actual child kinds: 2–5 for flat pairs or 6–9 for structural pairs.
The monotone threshold rule is unchanged.

The proving loop checks reconstructed subject counts against the schedule
before starting expensive work.

Singleton roots

ix aggregate imposes no two-shard minimum. The fold plan for one retained
leaf contains one shape-0 wrap and zero pairs; that wrapper and its CheckEnv
claim are persisted as the aggregate root. Even --direct-joins wraps a
singleton because the deliverable must verify under the one production
entrypoint. A self-pair would add work and manufacture a different statement.
Consequences:

  • exactly one proof is required per nonempty shard;
  • --plan-only reports one wrap and zero binary pairs for a singleton;
  • multi-shard flat/structural scheduling is unchanged; and
  • the final unconditional-root check always applies to an ix_aggr root.

ix aggregate

ix aggregate --ixe ENV.ixe --ixes SHARDS.ixes \ [--structural-above N] [--direct-joins] [--jobs N] [--max-ram G] [--no-cache] \ SHARD_PROOF_ADDR...

The command:

  1. deserializes the environment and manifest and verifies a disjoint exact
    shard cover on the raw manifest, then prunes zero-constant leaves;
  2. reconstructs every retained shard's exact CheckEnv claim and canonical
    trees (claim-only preparation — no dependency-byte closure);
  3. matches proof arguments to shards in any order by bundled claim digest,
    then requires exact claim equality and one proof per nonempty shard;
  4. natively verifies every IxVM shard proof before expensive recursion work;
  5. builds the IxVM and ix_aggr recursion systems and the pinned 80-byte
    allowed blob;
  6. derives every slot's statement, child kind, shape, uniform outer claim, and
    version-2 cache key up front;
  7. executes ready wraps and flat/structural pairs as a RAM-gated dependency
    DAG, resuming verified cached slots and persisting each completed wrapper
    (raw direct-policy leaves are verified inputs, not cache slots);
  8. canonicalizes all leaves under the final subject tree and checks that they
    reproduce the environment's canonical root;
  9. requires no remaining assumptions and natively verifies the outer proof at
    the one ix_aggr root entrypoint; and
  10. persists an Ixon.Proof containing the output CheckEnv claim and proof.

--plan-only stops after coverage validation and scheduling, resolving and
printing the scheduler policy without loading proofs or compiling recursion
systems:

ix aggregate --ixe ENV.ixe --ixes SHARDS.ixes \ --structural-above 0 --plan-only

ix verify --aggregate

Aggregate roots reuse Ixon.Proof, so verification is selected explicitly.
The verifier never accepts root-kind metadata. It serializes the bundled
CheckEnv value and reconstructs the only valid outer claim:

claim_bytes := serialize(CheckEnv(...))
outer_claim := ix_aggr(
blake3(allowed_blob),
blake3(claim_bytes))
InvocationCryptographic entrypointExpected statement value
ix verify --aggregate <proof>ix_aggrbundled claim; cryptographic proof only
ix verify --aggregate --ixe E <proof>ix_aggrcanonical environment root, unconditional
ix verify --aggregate --ixe E --ixes M <proof>ix_aggrpruned manifest fold, unconditional

Environment-only mode compares the bundled value to the canonical environment
root; a manifest-relative structural value therefore rejects naturally, with
no root-kind branch. Manifest-aware verification repeats pruning and the fold
schedule (the threshold must match the proving run), reconstructs every leaf
statement, folds the exact hybrid value, requires no assumptions, and verifies
the same ix_aggr outer claim. Existing non-aggregate proof and
shard-composition verification paths are unchanged.

Claim-only shard preparation

shardCheckEnvClaimTrees constructs only the CheckEnv claim and its
canonical subject/assumption trees. Aggregate planning, shard-digest
reconstruction, and aggregate verification use this path because none of them
consumes dependency bytes. shardCheckEnvClaim remains the witness-building
entry point; it reuses the same claim/tree result and computes the byte
closure once from the union of the owned and available primitive roots.
Reachability distributes over that root union, so this preserves the old
closure while avoiding repeated traversal of shared subgraphs.

Recursion parameters (WP-E1)

Aggregation and aggregate verification consume one shared
MultiStark.RecursionParameters value containing the recursion commitment and
FRI configurations, carried through runAggregateCmdWith/runVerifyCmdWith
into the shared MultiStark.buildRecursionSystem. The CLI entrypoints pass
defaultRecursionParameters, whose fields are exactly today's canonical Aiur
defaults. This is a mechanism split, not a policy change:

  • ordinary IxVM proving and verification remain on the canonical q=100
    configuration;
  • recursion proofs also remain q=100 with the same commitment and PoW values;
  • no new CLI flag selects security parameters; and
  • choosing q=50 plus a compensating PoW policy remains the explicit §9.5
    follow-up decision.

The recursion verifying key serializes and binds both parameter sets, and its
digest is included in every aggregate node's allowed-system blob, so any parameter
change changes the recursive protocol identity without a public-input or
allowed-blob format change. RecursionParameters.cacheFriBytes pins the
40-byte fri_params_ser cache-key component:

logFinalPolyLen:u64-le || maxLogArity:u64-le || numQueries:u64-le ||
commitProofOfWorkBits:u64-le || queryProofOfWorkBits:u64-le

Commitment parameters need no second cache component: changing them changes
the recursion-vk digest already present in the key.

Pruned FRI multiproofs (WP-E2)

The four-commit origin/update-multi-stark series is transplanted onto this
stack: Rust 1.98, the 249b7405 multi-stark revision on P3 v0.6, the matching
Blake3/Lean-FFI pins, and recursive proof-advice encoding. The new proof wire
format deduplicates shared Merkle siblings across sampled FRI queries. Width
and trace-height binding and the vk's checked max_multiplicity travel with
the dependency update; no aggregate claim or public-input format changes.

There are now deliberately two proof encodings. Proof.toBytes is the compact
wire form used by wrappers, native verification, the store, and WP-A cache
entries. Recursive execution still expects one path per query, so callers use
AiurSystem.proofToAdviceBytes immediately before a wrap or pair. The CLI,
pair/typecheck benchmarks, semantic tests, and activation audit all enforce
that boundary. Cached proofs stay compact at rest and are expanded only when
consumed as a parent pair's child.

The dependency's compact format is not backward-decodable. Store-facing CLI
verification therefore uses Proof.ofBytesChecked and reports a normal error
for legacy or corrupt proofs instead of crossing the unchecked decoder and
aborting. P3 v0.6 also requires a positive FRI query count; benchmark CLIs
reject --queries 0, and the old q=0 join smoke gate is replaced by q=1.
The Rust 1.98 migration also applies its new strict-Clippy equivalents for
fixed-size digest chunking and fallible metadata-size lookup; these are
mechanical, semantics-preserving rewrites.

Verified cache and resume (WP-A)

Before the first proof, the driver derives every fold slot's CheckEnv
statement, exact outer Aiur claim, and cache key, using the same
flat/structural fold policy as proving — so a manifest rebudget or statement
change invalidates only the changed subtree. The key:

blake3(
aggregateCacheVersion:u64-le ||
blake3(recursion_vk) ||
recursion_fri_params:40-bytes ||
serializeClaims([outer_claim])
)

The uniform outer claim already includes the ix_aggr entrypoint index and
public input, hence the allowed-system blob, output CheckEnv, both
verifying-key identities, and both accepted entrypoint indices. Each completed
recursive slot is stored as an Ixon.Proof wrapper in the content-addressed
~/.ix/store; a raw --direct-joins leaf is only an input and is not cached.
The wipeable index ~/.ix/cache/aggregate/<key> holds only the wrapper's store
address and is updated via temp-file + atomic rename. M1-d bumps
aggregateCacheVersion from 1 to 2, so old outer-claim entries miss cleanly.

A hit is an untrusted hint, not authority. Reuse requires all of: a
well-formed index address; store bytes whose Blake3 digest equals it; a
decodable wrapper; a bundled claim exactly equal to the precomputed slot
CheckEnv; a safely decoded Aiur proof (Aiur.Proof.ofBytesChecked — the
former trusted-byte constructor panicked on truncated bincode, and malformed
cache bytes must never abort the command); and native verification under the
exact precomputed outer claim and local recursion system. Any failure logs a
miss and re-proves. Cache write failures are warnings. On a valid hit the
host reconstructs the slot's preimages from the manifest statement, so
parents need no trusted metadata from the cache. --no-cache bypasses index
reads and intermediate writes while still persisting the final root wrapper.

RAM-gated parallel scheduling (WP-B)

The post-order fold executes as a dependency DAG: leaves are ready
immediately; a join becomes ready when both child slots have completed. The
controller sorts ready work by descending RAM weight (slot number breaks
ties) and starts one dedicated IO.asTask per admitted slot around the
existing proof FFI. Two independent limits govern admission:

  • --jobs N caps active slots; absent or 0 means every ready slot may run
    if it fits the budget;
  • --max-ram G caps total reserved GiB in flight, defaulting to 92% of Linux
    MemTotal.

As in the existing Rust RamGate, a slot whose estimate exceeds the entire
budget is admitted only while nothing else runs — a conservative estimate
serializes work rather than deadlocking. The calibration-pending per-shape
weights are:

raw IxVM leaf = 4 GiB
shape 0/1 wrap = 195 GiB
shape 2/6 IxVM + IxVM = 390 GiB
shape 3/4/7/8 mixed pair = 340 GiB
shape 5 self + self flat = 195 GiB + 1 MiB × subject leaves
shape 9 self + self struct = 195 GiB

The 195 GiB wrap reserve carries forward the recorded q=100 lift upper bound —
the measured peaks above (186.8–195.8 GiB) bracket it exactly. The 390/340 GiB
direct and mixed estimates deliberately keep the non-default policy out of the
256 GiB fleet tier pending M1-f calibration; measured self-pair peaks
(102.5–156.9 GiB) make their weights conservative in the safe direction. Real
calibration on a known-core box will tighten them without changing scheduler
semantics.

Results are installed by slot index, never arrival order; each worker gets an
immutable snapshot of its completed children, so parallel timing cannot
change parent advice. On failure the controller stops admitting, drains every
running proof, and reports the lowest failed slot; successful independent
tasks may finish publishing valid cache entries. Concurrent proofs share
Aiur's rayon global pool, so throughput can sub-scale even when memory
permits multiple slots; process-per-slot execution is a measurement-driven
follow-up.

Converged activation audit (WP-D + M1-e)

lake exe IxTests aggregate-activation is a dedicated diagnostic kept out of
the default suite. It executes the generated production ix_aggr path across
4 wraps and 128 pairs: two controlled child trace heights, shapes 0–9,
optional assumptions independently per side, complete discharge versus
explicit carry, and all four child-kind combinations in both flat and
structural modes. Every execution returns per-circuit (uniqueRows, totalHits); the runner performs the full 132-case matrix twice and rejects any
difference before rendering a deterministic per-shape Markdown report.
Current signature:

263a3087d42340613b012d50a9416e23549f85aa1ea425e86bf0a216c7b3e27d

Result: 183 audited circuits — 154 active in every case, 29 input-dependent,
zero never observed. The variable set concentrates in the expected paths:
wrap dispatch, canonical set folding, structural Merkle discharge, and
child-kind decoding. Fixed-height Bytes1/Bytes2 circuits are
outside the execute FFI's query-count array and are called out explicitly.
The audit records information only; dummy calls would add cost for the
currently planned SP1 terminal and should be chosen only if a future static
terminal requires input-independent activation.

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.executeMultiStarkJoin for generated or interpreted flat
    and structural join execution;
  • AiurSystem.proveMultiStarkJoin for witness generation and proving;
  • JoinAdvice, JoinPreimage, JoinTree, JoinPath, strict framed decoders,
    and the seven-channel join_io_buffer;
  • the matching rs_aiur_multi_stark_join_execute and
    rs_aiur_multi_stark_join_prove externs; and
  • Aiur.Proof.ofBytesChecked for panic-free store-boundary proof decoding.

The retained three-entrypoint executor is parity-checked against the bytecode
interpreter on both output and every circuit's unique-row/total-hit counts. The
combined verifier contains 247 Aiur functions; the regenerated Rust file is
2,239,894 bytes (ixvm: 5,807,375 bytes / 780 functions). Most line churn is
generated code. The protocol source of truth is
Ix/MultiStark/Aggregate.lean; the native advice contract lives in
aiur_multi_stark_runner.rs. It is now an Ix-agnostic regression/reference backend,
not the production CLI backend.

The production ix_aggr executor is independently parity-checked for wrap,
flat, and structural shapes. It contains 248 Aiur functions and its generated
Rust file is 2,061,361 bytes. Its source of truth is
Ix/Aggr/Circuit.lean, with native advice construction in
aiur_ix_aggr_runner.rs; all three generated artifacts are checked with
ix codegen --check.

Benchmarks

M1-f replaces the legacy two-shard/three-entrypoint runner with
bench-aggregate-policy, a production ix_aggr benchmark for one exact
four-shard fixture. It selects the four retained leaves from a validated and
pruned manifest, contracts the surrounding tree without changing orientation,
and independently reconstructs the expected host root. Wrap-first uses shape 0
leaves; direct mode keeps IxVM leaves raw. Both policies then select their flat
or structural heterogeneous shapes from the production plan. Measured runs
require q=100, jobs=1, --no-cache, and JSON output; the harness natively
verifies all four inputs and every recursive output, persists every completed
slot as an ordinary Ixon.Proof, and writes resumable per-slot and whole-run
timing/RSS/proof metadata.

The existing bench-typecheck --recursive --join row keeps its stable
join-* reporting/dashboard schema, but its join phase now exercises direct
ix_aggr shape 2 instead of the retired join_two circuit. Historical size
baselines remain recorded: 7,986,166 bytes (7,986,204 with the Ixon.Proof
wrapper) for a one-constant q=100 lift under the pre-E2 247-function system,
versus 7,443,023 bytes (7,443,061-byte wrapper) after WP-E2, a 543,143-byte /
6.80% reduction. Pruned multiproof length depends on sampled query-path
overlap, so a positive-PoW output is now a sample rather than an exact
byte-length pin. The deterministic q=100/PoW-0 regression signal is 7,447,279
bytes and 38,850,942,825 recursive-verifier FFT cost. The target-box policy
comparison is the remaining measurement, not remaining harness work.

Test coverage

The legacy aggregate-first suite uses a small stand-in child system so it can
create real Multi-STARK child proofs without the tens to hundreds of GiB
required by a full recursion proof. Those proofs are consumed by the retained
join circuits, so proof validation, statement decoding, set/path folding,
native advice, and transitive recursion are exercised end-to-end at execution
time.

The 57 passing legacy aggregate-first cases cover:

  • canonical flat and structural host folds; manifest parsing, validation, and
    post-order lowering;
  • distinct lift/flat/structural compiled entrypoint identities;
  • honest flat union and cross-child discharge; structural path discharge plus
    a carried assumption;
  • flat-join and structural-join outer claim layout and native verification;
  • transitively pinned flat and structural children; rejection of a child
    carrying a different allowed digest;
  • generated/interpreter parity for both join modes; threshold scheduling that
    is flat below and structural above monotonically;
  • strict keyed-blob framing; wrong-root and tampered-sibling paths; a missing
    path choice; a carried candidate omitted from the output; the obsolete
    88-byte allowed blob; a flat join fed a genuinely free-form structural
    child root; omitted/extra assumptions or subjects; noncanonical unsorted
    trees; tampered child proofs;
  • a synthetic three-leaf manifest with empty outer leaves: raw coverage before
    pruning, contraction, dense remapping, and singleton-lift classification;
  • claim-only shard preparation preserving exact subject/assumption trees,
    with the one-pass witness closure matching the legacy per-owned-root union;
  • exact legacy verifier reconstruction of a lift outer claim and native
    verification under it;
  • byte-for-byte equality of the shared default recursion builder with the
    former direct construction; the exact 40-byte FRI cache encoding;
    independent recursion-vk identity changes for FRI and commitment overrides;
  • cache-key invalidation across version, recursion vk, FRI parameters, and
    outer claim; whole-plan statement/claim/key derivation before proving;
    atomic index creation, corrupt-index detection and repair; a
    content-addressed, claim-bound, natively verified resume hit; mismatched
    claim rejection; corrupt store-content fallback; checked rejection of
    malformed proof bytes without a Rust panic;
  • heaviest-first admission with deterministic tie-breaking; job and byte
    ceilings; dependency release; admit-alone oversize handling; the flat-join
    affine weight and MemTotal parser; jobs=1/jobs=2 payload equality;
    failure draining without admitting a dependent join; jobs=1/jobs=2
    byte-identical recursive wrappers under canonical zero-PoW grinding, with
    each scheduled child decoded and verified; and
  • Rust writer normalization plus round-tripping a genuinely noncanonical
    structural CheckEnv root.

The wrapper-byte scheduler gate uses zero PoW deliberately: with the pinned
positive-PoW implementation, rayon's find_any selects any passing grind
witness, so two valid serial proofs already differ in bytes. Production
jobs=1/jobs=2 correctness is the stronger protocol invariant — exact claims
and native verification under the same recursion system. The primitive
Multi-STARK and recursive-verifier suites were rerun to guard the shared
verifier and lift paths, and the separate activation gate covers 132 accepted
shape/input cases twice.

The focused ix-aggr command now runs 93 passing checks. Its 42
circuit/shape/plan checks cover both wraps, all flat and structural child-kind
pairs, a nested structural self child, generated/interpreter parity, strict
tree and native keyed-blob framing, identity/shape closure, wrong or tampered
paths, omitted carries, unsorted trees, flat↔structural hint confusion, a flat
parent fed a free-form structural root, and exact four-shard wrap-first/direct
plans. Its 51 converged semantic checks cover uniform claims, cache v2 key
invalidation and verified resume, recursion parameters, manifest
validation/pruning, singleton/canonical/hybrid value reconstruction,
shard-preparation closure sharing, wrap-first/direct planning, per-shape RAM
admission, dependency/failure behavior, and jobs=1/jobs=2 determinism including
concurrent zero-PoW proving. Rust unit tests separately pin channel-6 key layout
and strict path-blob framing.

Validation run

All of the following pass on the current stack:

nix develop --command lake exe IxTests aggregate-first # 57/57nix develop --command lake exe IxTests ix-aggr # 93/93nix develop --command lake exe IxTests aggregate-activation # 132 cases × 2 stable passesnix develop --command lake exe IxTests multi-stark # 12/12nix develop --command lake exe IxTests recursive-verifier # 6/6nix develop --command lake build IxTests bench-typecheck bench-recursion-debug bench-aggregate-policy ixnix develop --command cargo test -p ixvm-codegen aiur_multi_stark_runnernix develop --command cargo test -p ixvm-codegen aiur_ix_aggr_runnernix develop --command cargo test -p ix-kernel shard::tests # 23/23nix develop --command cargo test -p ixon test_check_env_claim_accepts_structural_rootnix develop --command cargo clippy -p ixvm-codegen -p ix-ffi -p ix-kernel -p ixon --tests -- -D warningsnix develop --command cargo fmt --all -- --checknix develop --command lake exe ix codegen --checkgit diff --check

Both benchmark entrypoints also reject --queries 0 with a usage error before
building or proving, and a stored pre-E2 proof now reports a checked decode
error rather than aborting. The fresh post-E2 aggregate fixture verifies
against both its .ixe and .ixes inputs.

A q=1 live smoke of the converged typecheck join also executed, proved, and
verified direct shape 2 successfully: 1.221742 s execution, 26,738,665,748 FFT
cost, 52.380108 s proving, 21,565,714,432-byte peak RSS, 401,340-byte proof,
and 0.002616 s verification. The M1-f policy harness passed plan-only smoke for
both policies and emitted the expected resumable JSON schema.

Pre-convergence real-environment gates on the 124 GB development host (kept
as WP-A/WP-B baselines for the M1-f rerun):

  • A two-shard manifest from the 5,986-constant tc-parity.ixe environment
    passes coverage and structural scheduling with threshold zero
    (2 lifts + 1 binary joins (1 structural)), and its one-shard variant
    plans 1 lifts + 0 binary joins.
  • Before E2, a one-constant q=100 production lift ran end-to-end through the
    cache and scheduler, establishing WP-A's invalidation/repair baseline:
PassResultWall
Initialproved and cached root 090bea6f…ca1535~64 s
Identical rerunverified cache hit, same root1.66 s
Truncated indexclean miss, re-proof, atomic repair, same root~38 s
Post-repair rerunverified cache hit, same root1.79 s
--jobs 2 --max-ram 400195 GiB reserved, verified hit, same root1.45 s

At scale, the measured pre-E2 4-shard Init run in Measured performance
above exercised the former backend with real shard proofs, four q=100 lifts,
two lower joins, and a root join with genuine assumption discharge. The pinned
M1-f harness will repeat this fixture under both converged policies.

Soundness summary

  • Every present child is fully verified under the verifying key selected by
    its shape-constrained kind.
  • Child function indices are constrained to IxVM verify_claim or ix_aggr,
    and every recursive child must carry the same allowed digest, pinning the
    two verifying keys and two entrypoints transitively.
  • Claims and allowed data are Blake3-bound and strictly parsed with no trailing
    bytes.
  • Flat mode reopens canonical roots and proves exact union/difference;
    structural mode proves the exact root-of-roots relation and accounts for
    every unique input assumption through either inclusion or explicit carry.
  • A bogus path, dropped candidate, extra survivor, malformed choice, or
    noncanonical assumption tree rejects. Over-carrying and duplicate subject
    leaves can only weaken the output claim; the CLI requires an unconditional
    final root.
  • Coverage is checked on the unmodified manifest, so pruning cannot conceal a
    hole or overlap; only leaves with an environment-derived owned-constant
    count of zero are removed, and contraction preserves the order and shape of
    retained leaves.
  • A singleton root is a verified shape-0 wrapper, never a raw-proof shortcut;
    the circuit binds its output digest directly to the pinned IxVM child's
    CheckEnv digest.
  • Manifest-aware verification independently repeats pruning, scheduling, and
    value folding, then verifies the one derived ix_aggr outer claim. No
    claimed proof kind or shape is accepted from wrapper metadata.
  • Proving and verification build the recursion system from the same explicit
    parameter object; any parameter change necessarily changes the recursion vk,
    allowed-system identity, and cache key.
  • Cache keys bind version, recursion-vk digest, explicit FRI bytes, and exact
    outer claim, all derived before proving; hits re-hash store content, bind
    the claim, decode without panicking, and verify natively. Corrupt, missing,
    or stale entries are misses, never trusted successes or command failures.
  • A pair is admitted only after both child slots complete; immutable
    snapshots and indexed installation make arrival order irrelevant. The
    controller owns all reservations; failures drain running work before exit.
  • Free-form structural roots are serialized as opaque commitments
    (crates/kernel/src/claim.rs); canonical openings are required only by
    protocol paths that explicitly need them.

Review map

Commit-by-commit review is recommended — each commit is feature-scoped with
its own tests (map in the first comment / commit list).

AreaPrimary filesWhat to review
Legacy regression protocolIx/MultiStark/Aggregate.lean, Ix/MultiStark.leanpre-convergence 96-byte identity and semantic/audit reference surface
Production circuitIx/Aggr/Circuit.lean, Ix/Aggr.lean80-byte identity, uniform claims, shapes 0–9, heterogeneous child pinning, structural path checks
Converged host modelIx/Aggr/Host.leanfree-form structural roots, canonical survivors, path extraction
Host statement modelIx/MultiStark/Host.leanfree-form subject roots, canonical assumptions, path extraction
Manifest parsing + pruningIx/Cli/CheckCmd.leanstrict tree parsing, validate-before-prune, unary contraction, dense remap
Aggregate driverIx/Cli/AggregateCmd.leanwrap-first/direct shape policy, singleton wrap, uniform slot claims, cache v2, per-shape RAM weights, verified DAG resume/failure draining
Root verificationIx/Cli/VerifyCmd.leanone ix_aggr outer claim, value-based environment comparison, manifest-relative hybrid reconstruction
Recursion parametersIx/MultiStark.lean, Ix/Aggr.lean, Ix/Cli/{Aggregate,Verify}Cmd.leanshared config plumbing, deterministic systems, stable FRI cache encoding
Pruned FRI multiproofsIx/Aiur/{Protocol,Semantics/BytecodeFfi}.lean, Ix/Cli/{Aggregate,Verify}Cmd.lean, crates/ffi/src/aiur/protocol.rscompact persisted proof vs expanded recursive advice boundary, checked legacy-proof failure
Cache/store boundaryIx/Store.lean, Ix/Aiur/Protocol.lean, crates/ffi/src/aiur/protocol.rswipeable namespace, checked proof decoding, corrupt-data fallthrough
Shard preparationIx/IxVM/ClaimHarness.lean, Ix/Cli/{Aggregate,Check,Verify}Cmd.leanclaim/tree-only consumers, one-pass multi-root witness closure
Native advicecrates/ixvm-codegen/src/aiur_multi_stark_runner.rsstrict framing and seven-channel key layout
Converged native advicecrates/ixvm-codegen/src/aiur_ix_aggr_runner.rs, crates/ffi/src/aiur/protocol.rsstrict path framing, shared channel-6 key layout, execute/prove passthrough
Generated executorcrates/ixvm-codegen/src/aiur_multi_stark.rsgenerated artifact; verify with ix codegen --check
Converged generated executorcrates/ixvm-codegen/src/aiur_ix_aggr.rsgenerated artifact; verify with ix codegen --check
Manifest writercrates/kernel/src/shard.rsproducer-side omission, tree remap, aggregate metadata
Claim semanticscrates/kernel/src/claim.rs, crates/ixon/src/proof.rsfree-form root contract and codec regression test
TestsTests/Aggr.lean, Tests/AggrSemantics.lean, Tests/AggrActivation.lean, Tests/MultiStark.lean93 production shape/semantic/plan checks, 132-case activation audit, retained Ix-agnostic regressions
BenchmarksBenchmarks/AggregatePolicy.lean, Benchmarks/Typecheck.lean, bench registry/dashboardspinned four-shard wrap-first/direct handoff, resumable JSON, converged direct-shape-2 join metrics, historical lift-size pins

Current limits and non-goals

  • Recursion FRI parameters stay at the q=100 defaults; the q=50 + compensating
    PoW policy is an explicit follow-up decision (the measured costs above are
    therefore the conservative end).
  • Structural roots are manifest-relative. Environment-only verification
    accepts only a bundled value equal to the canonical environment root;
    hybrid roots should be verified with both --ixe and --ixes and the
    proving threshold.
  • Scheduler RAM weights are calibration-pending placeholders (validated in
    the safe direction by the measured run); concurrent proves share the rayon
    global pool, so throughput can sub-scale — process-per-slot is a
    measurement-driven follow-up.
  • Same-key cross-process cache writes degrade to a warning and recomputation,
    never a soundness issue.
  • Dummy-call activation padding is deferred until/unless a static terminal
    circuit is selected; under the SP1 terminal it is pure cost.
  • Aggregate wrappers reuse Ixon.Proof and do not encode the proof system;
    callers must pass ix verify --aggregate.
  • M1-a–f land the converged production circuit, host/native substrate, driver,
    cache, scheduler, value-based verifier, semantic union, widened activation
    audit, and policy benchmark harness. The only remaining M1 work is executing
    the wrap-first/direct comparison on the target box. The pinned M1-f revision,
    inherited M1-e preflight, and both measured policies are one checkout/session;
    its handoff is documented in
    plans/aggregate-first-m1f-large-box-runbook.md.
  • This PR does not adopt cold-circuit grouping and does not implement a
    terminal KZG/SP1 wrapper.

Follow-ups

  1. Calibrate lift/join unit costs on a known-core box, with per-node
    flat-vs-structural attribution, and measure frontier/discharge-path counts
    at Mathlib scale (plan §11.4.1).
  2. Decide the recursion q=50 + PoW policy (§9.5) now that the mechanism
    exists.
  3. Connect the root proof to the selected terminal compression path (E3:
    single-recursion-proof SP1 guest + ix compress-root).
  4. Re-evaluate cold-circuit grouping (E4); E2's measured outer-lift reduction
    was only 6.80%, so grouping may still be material.
  5. Process-per-slot proving if measured rayon contention warrants it.
  6. Run the exact M1-f commit
    b70042d6d1a029bb8e1d541da7aef5e0613d67e6 for the combined M1-e preflight
    and wrap-first/direct policy benchmark session in the large-box handoff
    runbook.

johnchandlerburnhamand others added 22 commits August 27, 2026 16:21
Add the deterministic lift/flat/structural activation matrix and keep dummy calls deferred. Split aggregate recursion commitment/FRI configuration from IxVM defaults, share it between proving and verification, and pin the future cache encoding without changing active protocol parameters.
Precompute versioned per-slot cache keys, persist lift and join wrappers in the content-addressed store, and reuse entries only after exact claim and native outer-proof verification. Add safe proof decoding, corruption recovery, and --no-cache.
Execute ready lift and join slots as a dependency DAG under explicit job and RAM admission. Add calibration-pending slot weights, failure draining, CLI controls, and serial/parallel scheduler and proof-equivalence gates.
Add an opt-in two-child benchmark that proves singleton CheckEnv shards, lifts both proofs, and measures a verified flat join. Wire join metrics through reporting and dashboards, and record the current pre-E2 lift-size baseline.
- rust-toolchain.toml channel: 1.92 → 1.98, with the matching fenix
toolchain hash in flake.nix. The pinned fenix already carries the 1.98
release manifest, so flake.lock needs no change (fenix's nixpkgs stays
pinned via its lean4-nix follows, so the Lean toolchain is untouched).
- Drop clippy::from_iter_instead_of_collect from the workspace lints:
removed in clippy 1.98 and now warns as unknown.
- Fix the warnings new clippy 1.98 lints surface across the workspace:
chunks_exact(N) → as_chunks::<N>() where the chunk size is constant,
descending sort_by → sort_by_key(Reverse(..)), iteration over map
values via .values(), map().unwrap_or() → map_or(), a checked
division, an unwrap-after-is_some restructured into if-let, and
assorted redundant-reference/pattern cleanups (mostly cargo clippy
--fix). The two byte-gadget files keep their chunks_exact warnings
until the next commit, which rewrites those regions anyway.
CI derives its Rust version from rust-toolchain.toml, so no workflow
changes are needed.
Companion to multi-stark's update-p3 branch (c72d321 → 249b740), which
carries four soundness/robustness fixes and the Plonky3 v0.6.0 bump
(pruned FRI Merkle multiproofs: ~2x faster verification, 40-70% smaller
proofs; canonical Goldilocks serde removes proof-byte malleability).
Proofs and verifying keys are not compatible with the previous pin.
Integration:
- Lookup gained max_multiplicity, a declared per-row bound on the
multiplicity's integer magnitude feeding the newly enforced logUp
height bound Σ wᵢ·hᵢ + |claims| < p. Function-circuit slots accumulate
mutually-exclusive branch selectors, so they declare 1; committed
count columns (function return slots, the memory circuit, the byte
gadget tables) declare the new COUNT_COLUMN_BUDGET (2^32 queries per
entry).
- The VK wire format carries the bound: u64 LE max_multiplicity per
lookup, between the multiplicity node id and the arg count. The
in-circuit VK deserializer (Ix/MultiStark/SystemDeserialize.lean)
parses past it; the value is bound through the vk digest but the
height bound itself is not yet enforced in-circuit (the native
verifier enforces it).
- Message fingerprints are width-bound by default upstream (the slot
width seeds the Horner fold), which is incompatible with aiur's
branch-shared lookup slots: mutually exclusive branches superpose
messages of different natural widths into one slot at the maximum
width, so a narrow call is sent zero-padded to a width its callee's
return slot never provides, and proving fails with
UnbalancedChannel (pinned as the prove_verify_mismatched_call_widths
regression). Aiur instead declares WidthBinding::ByConstruction —
the plain Horner fold, restoring zero-padding transparency — and
takes on the prefix-freeness contract that makes it sound: every
message's natural width is a function of its constant-constrained
leading prefix (channel tag plus discriminator: fun_idx fixes
2+in+out, the memory size coordinate fixes 3+size, each gadget tag
fixes its table width), so zero-extension can only equate a padded
message with its own natural form. The contract is documented at the
channel constants in lib.rs; the declaration is applied in
AiurSystem::build and mirrored in the vk_codec decoder so decoded
VKs replay the same transcript. ByConstruction is also exactly the
fold the in-circuit verifier's logup_fingerprint already computes,
so the recursive verifier needs no fingerprint change.
- The policy is Fiat-Shamir-bound as the first observe_shape word; the
in-circuit transcript replay prepends the matching limb.
- aiur_multi_stark.rs regenerated (ix codegen) for the deserializer
and transcript changes.
- P3 v0.6.0 ships FRI query openings as pruned Merkle multiproofs,
while the in-circuit verifier consumes one authentication path per
query (its per-query control flow is a far smaller circuit than the
amortized multiproof walk). Rather than porting the walk into the
DSL, the proof advice stays in the per-query transport: multi-stark's
new advice module re-encodes a natively-verified proof by running
p3's own verification with a recording compression function and
reading each query's path back out of the recorded digest map. The
advice bytes are untrusted verifier input, never digest-bound —
the transcript binds the commitments and every expanded sibling is
authenticated against them per query — so pruning vs expansion is
pure transport and the encoding choice is sound. AiurSystem gains
proof_to_advice_bytes (FFI: AiurSystem.proofToAdviceBytes); the
recursive-verifier test feeds it instead of Proof.toBytes, whose
native wire format is still round-tripped separately. The Lean-side
proof grammar and the codegen'd verifier are byte-identical to
before — no in-circuit changes.
Claim layout, the VK wire format above and aiur's public semantics are
otherwise unchanged; the policy adds no prover or verifier work over
the previous pin.
Still open, native-verifier-only: the logUp height bound is parsed
past but not yet enforced in-circuit (a wide-arithmetic check, tracked
separately).
The Lean v4.33.1 update pinned both dependencies at revisions that
predate their Rust 1.98 bumps; their heads now carry those bumps, which
this workspace needs since rust-toolchain.toml moved to 1.98. Both
revisions stay on leanprover/lean4:v4.33.1.
- Blake3.lean 1b0fbd2 → e6e908b (Rust 1.98, plus a case-insensitive
source-directory fix), updated in lakefile.lean, lake-manifest.json
and the blake3-lean flake input. The revision keeps the
`blake3_rs_shared` target the `ix_native_decide_dynlib` pin requires.
The inherited entry in Benchmarks/Compile/lake-manifest.json was still
on the pre-4.33.1 revision and now tracks the root pin.
- lean-ffi 2a9c91e → 93c7e52 (Rust 1.98). Only bignat reaches the sp1
and zisk workspaces, so their lock files move that one package.
The !benchmark recursive phase reported n/a for every fri-verifier
metric: Benchmarks/Typecheck.lean still fed Proof.toBytes — the pruned
multiproof wire format — to executeMultiStark/proveMultiStark, so the
in-circuit verifier rejected on parse and the harness (correctly) left
the recursive fields absent rather than emit a fake datum. The
in-circuit verifier consumes the per-query advice transport
(AiurSystem.proofToAdviceBytes); proofBytes stays the reported
proof-size metric.
bench-recursion-debug had the same advice-format gap plus a stale
claim recipe: it still built the public input as 32 raw digest bytes,
predating the ClaimHarness.packedDigestKey packing bench-typecheck
uses (its own out-of-circuit sanity check failed with
InvalidPowWitness — a wrong claim diverges every challenge — and the
advice re-encoder refused the proof for the same reason). Both aligned
with the typecheck flow.
Validated end-to-end at production parameters (numQueries 100,
query PoW 20, blowup 2) on Nat.add_comm: inner prove, advice
re-encoding, and the codegen'd in-circuit verifier accepting.
Keep compact proof bytes at storage and cache boundaries, expand them only for recursive lift/join advice, reject unsupported zero-query benchmarks, and decode legacy store proofs without panicking. Refresh tests, benchmark pins, and Rust 1.98 lint compatibility.
Add Ix/Aggr, a recursive aggregation system for IxVM shard proofs that
keeps Ix/MultiStark untouched and Ix-agnostic. One entrypoint, ix_aggr,
subsumes lifting and joining: a one-byte advice shape selects wrap or
binary join over any mix of IxVM and ix_aggr children, so shard proofs
enter the recursion system directly as join children and the dedicated
lift stage disappears.
Circuit (Ix/Aggr/Circuit.lean): every shape verifies its children in
full (verify + ood_verify from the shared Multi-STARK verifier modules)
against the vk its hinted kind demands. Identity is one 80-byte
digest-bound blob - blake3(ixvm vk) || verify_claim idx ||
blake3(self vk) || ix_aggr idx - carried unchanged at every node; self
children must bind the identical blob digest, pinning both vks and both
entrypoint indices transitively. Claims are a uniform 18-word
[0, aggr_idx, allowed(8), checkEnv(8)] at every depth, so proofs of
different tree levels combine freely. Wrap shapes bind the output
digest to the child CheckEnv digest directly; pair shapes open both
CheckEnv preimages, re-root the canonical subject/assumption trees, and
prove subjects = L ∪ R, assumptions = (asmL ∪ asmR) ∖ subjects with
linear sorted merges.
Toplevel (Ix/Aggr.lean): ixAggr = MultiStark.multiStarkFull + circuit,
pruned to ix_aggr, so verify_multi_stark_proof and other unrelated
entries no longer pad aggregate proofs. The host half of the wire
contracts (allowed blob, public input packing, shape codes, keyed
preimage/tree blob framing, interpreter IO assembly) lives beside the
toplevel; Ix/Aggr/Host.lean folds CheckEnvTrees statements.
Native path: ix codegen gains the ix-aggr target
(crates/ixvm-codegen/src/aiur_ix_aggr.rs, 244 fns); its runner builds
the seven-channel IO buffer natively and routes execution through the
generated code. New FFI rs_aiur_ix_aggr_execute/_prove
(executeIxAggr/proveIxAggr) pass proofs, vks, claims, and the compact
count/key/length preimage/tree blobs without per-byte boxing, plus
Proof.ofBytesChecked for store-boundary decoding.
CLI: ix aggr --ixe E --ixes M <shard-proof>... reconstructs every
nonempty shard statement from the env, matches wrappers by claim
digest, natively pre-verifies them, folds a balanced bisection (the
canonical fold makes the root claim independent of tree shape), wraps
single-shard roots so the persisted root is always an ix_aggr proof,
checks the root closes over the env canonical tree with no residual
assumptions, and persists the wrapper.
Tests (lake test -- ix-aggr, 17 cases): all five shapes accept over
real Multi-STARK stand-in child proofs from two distinct-vk systems;
codegen'd execution matches the interpreter on output and per-circuit
query counts for wrap and pair; negatives break one binding each -
lying shape hint, tampered proof, foreign identity, wrap statement
drift, dropped assumption, padded subject set, and tree advice not
reproducing its keyed root.
Replace the legacy lift/join entrypoints with the heterogeneous ix_aggr system across aggregate, verification, cache, and codegen paths. Preserve wrap-first and direct-join policies behind one proof identity and cache namespace.
Add the converged 91-check semantic suite and a deterministic 132-case activation audit covering shapes 0 through 9 twice, with no unobserved circuits.
Replace the legacy two-shard three-entrypoint benchmark with a four-shard manifest-subtree harness for production ix_aggr wrap-first and direct policies. Enforce the q=100 serialized no-cache profile, natively verify every input and recursive output, persist ordinary aggregate wrappers, and emit resumable per-slot JSON metrics.
Port bench-typecheck --join to ix_aggr direct shape 2 while preserving the stable join metric schema. Remove the final legacy slot-spec/preimage shim and pin both M1-f four-shard plans in the focused suite.
Update the multi-stark dependency and Rust toolchain for Plonky3 0.6, along with the Rust 1.98 lint migrations required to keep the workspace warning-free. Refresh the Rust-compatible Blake3.lean pin in both root and compile-package manifests.
Adapt recursive Aiur verification to Plonky3's pruned FRI multiproofs. Native proofs retain their compact serialized representation and native verification path; the FFI expands authenticated Merkle frontiers into per-query advice only when entering the existing recursive verifier circuit.
Preserve the packed claim-digest convention in the recursion diagnostic and exercise the advice boundary in the end-to-end test and benchmark paths. CPU and CUDA recursive q1 runs produce identical 823,485-byte inner proofs and 331,273-byte outer proofs.
The q50 Vector.extract_append workload retains identical CPU/CUDA proof sizes. Inner plus outer STARK proving measures 65.87s on CPU and 8.81s with CUDA on the RTX PRO 6000, a 7.48x speedup.
PR benchmark runs execute trusted workflow YAML from the default branch while loading composite actions from the PR checkout. When Bencher data and binary caches are unavailable, the workflow checks out main under base/ and asks Lake to rebuild it without first installing the Rust channel pinned by that checkout.
Teach the existing CPU provenance action to install the base checkout's validated Rust channel and profile immediately before an uncached base build. The step is a no-op when the toolchain is already available and leaves cached benchmark comparisons unchanged.
Consume the Plonky3 0.6 batch-opening layout directly in Aiur instead of expanding every pruned Merkle frontier into one authentication path per FRI query. Sample all query indices from the unchanged transcript, sort and deduplicate them with an O(q log q) merge sort, authenticate each input and commit-phase commitment once, then retain the existing per-query reduced-opening and FRI arithmetic.
Bind every frontier to transcript-derived indices, consume boundary digests in Plonky3's level/parent/child order, reject trailing frontier elements and inconsistent duplicate leaves, and assert all native opening dimensions and sibling counts. Explicitly constrain the digest-bound protocol specialization to cap height 0, binary FRI, and a constant final polynomial. Move memo_u32_less_than into IxVM Core so both substitution and multiproof sorting share its constrained rows.
Strengthen the recursive negative test to mutate a structurally valid stage-1 commitment. Regenerate both checked-in Aiur Rust executors and retain interpreter/codegen query-count parity.
On Vector.extract_append q50, recursive-verifier FFT cost falls from 204.073B to 201.166B. CPU outer proving improves from 50.09s to 45.03s and the full CPU pipeline from 90.64s to 82.90s. GPU outer proving improves from 15.85s to 13.72s and the full GPU pipeline from 28.86s to 26.69s. The outer proof grows from 3.92 MB to 4.17 MB.
Validated with the MultiStark primitive suite, recursive honest/tamper/parity tests, codegen --check, release workspace clippy, release CUDA clippy, rustfmt, and diff checks.
@johnchandlerburnham

Copy link
Copy Markdown
MemberAuthor

Init full proving report

Revision: 82dcc9dba36d0cfd6c753c6878ab649643a52026
Host: 64 vCPU, 495 GiB RAM, 256 GiB swap
Final result: one verified ix_aggr FRI proof with no remaining assumptions.

Compilation and sharding

StepResult
Compile sourceBenchmarks/Compile/CompileInit.lean
Ix compiler time1.876s
Constants compiled65,994
init.ixe size194,296,853 bytes (185.3 MiB)
Static sharding time3.1s
Manifest16 nonempty shards
Retained/owned constants56,621
Manifest size2,619,404 bytes (2.50 MiB)
Predicted FFT per shard4.160e11–4.197e11
Predicted FFT spread1.01×

The 1.876-second compile measurement is the Ix compiler itself; it excludes the initial one-time dependency download and Lake build.

Stage 1: base shard proofs

All 16 shard proofs were persisted and individually verified against their corresponding manifest claims.

MetricSerial baselineTwo-way run used for aggregation
Concurrency12
End-to-end wall time24m 16.76s15m 53.93s
Sum of shard prover times22m 43.23s30m 08.14s
Per-shard time range74.08–113.97s98.94–142.29s
Median shard time82.66s108.75s
Highest individual RSS219.8 GiB219.8 GiB
Combined process-tree peak358.2 GiB
Proof count1616
Total proof-wrapper size369,995,038 bytes
Mean proof-wrapper size23,124,690 bytes
Proof-wrapper range22,714,520–23,998,692 bytes

Two-way concurrency gave a 1.53× wall-time speedup despite increasing total prover-seconds by about 33% from CPU/memory contention.

Stage 2: recursive aggregation

Policy:

  • Wrap-first
  • --structural-above 4096
  • --jobs 2
  • --max-ram 450
  • Default q=100 recursion parameters
  • Aggregate cache enabled

Plan:

OperationCount
Shard wraps16
Binary joins15
Structural joins14
Flat joins1
Total recursive slots31

Results:

MetricResult
Wall time45m 06.61s
Peak resident memory379.9 GiB
Average CPU utilization4,192% (~42 cores)
Cache hits0
Freshly proved/cached slots31
Swap used0 bytes
Exit status0
Final proof-wrapper size9,596,044 bytes (9.15 MiB)

Final proof

Root address:

80f6844f968920abaef8d8c750a1a1dc1ebe32657a3cfb824e1810bcd65c8814

Final claim:

CheckEnv(6dc8f821a989c6f7b9d520b053ef8edbd582d2408d7b6a6ff72b908666e03b6f, none)

Both validations passed:

  • Cryptographic-only aggregate verification
  • Manifest-aware verification against init.ixe, init-16.ixes, and structural threshold 4096

End-to-end measured time

Using the two-way Stage 1 run:

ComponentTime
Ix compilation1.876s
Sharding3.1s
Stage 1 proofs15m 53.932s
Stage 2 aggregation45m 06.61s
Measured core pipeline1h 01m 05.52s

This total excludes the one-time dependency build/download and a few seconds of manifest and final-root verification overhead. Using serial Stage 1 instead would make the measured core pipeline approximately 1h 09m 28.35s.

Use the PR77 lean-ffi and multi-stark pins, remove the stale width-binding and multiplicity-bound protocol fields, and regenerate the recursive and aggregate verifier sources.
Expose an opt-in x86_64-linux cuda shell with NVCC 13.2, sm_120 code generation, and safe host libcuda discovery while leaving the default development shell unchanged.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@johnchandlerburnham@samuelburnham@arthurpaulino
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root - #598

Open
johnchandlerburnham wants to merge 24 commits into
mainfrom
jcb/aggregate-first
Open

Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root#598
johnchandlerburnham wants to merge 24 commits into
mainfrom
jcb/aggregate-first

Conversation

@johnchandlerburnham

@johnchandlerburnhamjohnchandlerburnham commented Aug 27, 2026

Copy link
Copy Markdown
Member

Summary

This PR implements the aggregate-first path for Aiur Multi-STARK proofs. The
production path now runs through one ix_aggr recursion entrypoint: it wraps
independently-proven IxVM shards, folds their CheckEnv statements along the
manifest's bisection tree, and persists one recursive root proof with a
uniform 18-word outer claim. Every completed recursive slot is persisted in a
verified, content-addressed version-2 resume cache, and ready slots are proven
in parallel under explicit job and RAM limits.

The entrypoint has two pair-fold modes:

  • Flat shapes perform a canonical set union/difference and are efficient for
    small lower nodes.
  • Structural shapes commit to nodeHash(leftRoot, rightRoot) in one hash and
    discharge assumptions with Merkle inclusion paths. Their 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 (default
4096 subject leaves): flat below the threshold, structural above it.

IxVM shard proofs
│
├── ix_aggr shape 0 (default wrap-first policy)
│ or remain raw with --direct-joins
▼
ix_aggr proofs carrying uniform CheckEnv statements
│
├── flat ix_aggr shapes 2–5 below the configured subject threshold
│ subjects := canonical(subjectsL ∪ subjectsR)
│ assumptions := (assumptionsL ∪ assumptionsR) ∖ subjects
│
└── structural ix_aggr shapes 6–9 above the threshold
subjects := nodeHash(subjectRootL, subjectRootR)
assumptions := candidates carried or discharged by inclusion path
▼
Ixon.Proof {
claim := CheckEnv(manifest-relative subject root, none),
proof := one recursive Multi-STARK root proof
}

A manifest with one retained (nonempty) shard is also a valid deliverable: its
root is one shape-0 wrapper, with no self-join padding (including under
--direct-joins). Zero-constant manifest leaves are pruned after raw coverage
validation:

raw manifest
|
| validate exact environment coverage before pruning
v
drop zero-constant leaves + contract unary tree nodes + remap retained ids
|
+-- one retained leaf --> wrap once and persist the ix_aggr root
|
`-- multiple leaves --> fold flat/structural joins as above

This is the stage-2 aggregation layer from
plans/aggregate-first-pipeline.md: shard proofs are aggregated before any
terminal KZG/SP1 compression, so a later wrapper only needs to consume one root
proof. Terminal compression remains outside this PR.

Section 14 implementation is complete through M1-f in this branch. The
ap/ix-aggr single-entrypoint base is reconciled with WP-E2's compact stored
proof / expanded recursive-advice boundary, and its one ix_aggr entrypoint
supports wraps, flat pairs, and structural pairs across both IxVM and recursive
children. ix aggregate now uses that backend for planning, proving, cache
resume, and final verification; ix verify --aggregate derives one uniform
outer claim and performs value-based environment/manifest checks. The old
ix aggr command and all three-entrypoint production wiring are retired. The
older Multi-STARK aggregate implementation remains as an Ix-agnostic regression
surface, not as a CLI backend. Production semantics, activation coverage, and
benchmarking now live entirely against ix_aggr. M1-f lands the pinned
four-shard bench-aggregate-policy handoff and ports the stable typecheck join
metric to direct shape 2; only execution of the policy comparison on the large
box remains. Operationally this is one future handoff: Arthur will check out
b70042d6d1a029bb8e1d541da7aef5e0613d67e6 once, run the inherited M1-e
correctness suite as untimed preflight, and then benchmark both policies. There
is no separate M1-e box run.

Measured performance (2026-08-29)

Arthur Paulino ran the pre-convergence pipeline end-to-end on four real Init
shard proofs (shards 8–11 of a 16-way partition, 12,493 constants; base shard
proving excluded; native verification of each output included; ~512 GB box).
These are the standing M1-f comparison baseline; the converged wrap-first and
direct policies have not yet been rerun on that box. The recursion used the
conservative q=100 / PoW 20 defaults this PR keeps:

StageTimePeak RAMOutput proof
Lift × 498.8–103.0 s (mean ≈ 101 s)186.8–195.8 GiB8,162,462 B each
Join 8+9, 10+1148.9 / 48.5 s~102.5 GiB9,502,843 B
Root join85.8 s156.9 GiB9,455,498 B
Total (serialized)10:23 wall195.8 GiB9,455,498 B

Notes: lift cost is shard-content-independent (four lifts within ±2%, tracking
the ~constant 20–23 MB shard-proof shape); the measured lift peak brackets the
scheduler's 195 GiB weight placeholder exactly; and an unserialized run of all
four lifts OOM'd at ~492 GiB — precisely the behavior the WP-B admission gate
prevents (on a 512 GB box its weights admit two lifts). Because shards 8–11
are a strict partition subset, the root carries a real frontier assumption
set, so the joins exercised genuine discharge work. Full analysis, including
the comparison against direct (unlifted) IxVM joins, is in
plans/aggregate-first-pipeline.md §3.4 and the Zulip thread.

What lands

Protocol / circuit layer:

  • One production recursion entrypoint, ix_aggr, under one verifying key.
    Shapes 0–1 wrap one IxVM/recursive child, shapes 2–5 fold flat pairs, and
    shapes 6–9 fold structural pairs for every child-kind combination.
  • Recursive validation of wrap, flat-pair, and structural-pair children with
    transitive verifying-key, function-index, and allowed-system binding. A
    shape hint selects the verified form but cannot weaken its checks.
  • Strict in-circuit CheckEnv decoding and canonical flat set folding.
  • Structural subject folding with per-candidate Merkle discharge paths while
    keeping output assumption sets canonical and deduplicated.
  • One 80-byte protocol identity containing the IxVM/self verifying-key
    digests and their two accepted entrypoint indices; every recursive node has
    the same 18-word outer-claim layout regardless of shape.
  • Structural outputs bind nodeHash(leftRoot, rightRoot) and account for each
    unique assumption candidate through a strict Merkle-path-or-carry choice.
  • Host helpers construct structural roots, survivors, and path advice. Channel
    6 carries both the one-byte shape at key [0] and candidate path payloads at
    raw 32-byte address keys; the key shapes are disjoint.
  • Lean/Rust execute and prove FFI accept a compact keyed path blob, whose
    framing is decoded strictly before the circuit independently validates
    payload semantics.
  • The generated ix_aggr executor is regenerated at 2,061,361 bytes / 248
    Aiur functions and parity-tested against the interpreter.

Host driver and operations:

  • Strict .ixes aggregation-tree parsing and post-order lowering, with a
    balanced fallback for legacy manifests; validate-then-prune removal of
    zero-constant leaves with unary tree contraction (and a Rust writer that
    stops emitting them).
  • Single-shard roots: a one-retained-shard manifest always produces one
    shape-0 ix_aggr wrapper, including under --direct-joins.
  • A monotone leaf-count scheduler controlled by --structural-above, with a
    default wrap-first plan (shapes 0 then 5/9) and an explicit
    --direct-joins plan that derives heterogeneous shapes from actual child
    kinds.
  • Versioned, content-addressed aggregate cache and resume (WP-A): version 2
    binds the uniform outer claim, and reuse occurs only after content-digest,
    claim, decode, and native verification all pass; --no-cache bypasses.
  • Parallel execution of ready wraps/pairs as a dependency DAG under --jobs
    and --max-ram (default 92% of MemTotal), heaviest-first, with failure
    draining (WP-B). Admission weights are shape-aware: 4 GiB for a retained
    raw IxVM leaf, 195 GiB for wraps/self pairs, 390 GiB for IxVM/IxVM pairs,
    and 340 GiB for mixed pairs.
  • A recursion-parameter split (WP-E1): aggregate proving/verification share an
    explicit RecursionParameters value, defaulting to today's q=100 values;
    the q=50+PoW policy decision is deliberately deferred.
  • A deterministic 132-case production activation audit across shapes 0–9:
    two controlled child trace heights, both wraps, every flat/structural
    child-kind pair, optional assumptions per side, and discharge/carry. The
    complete matrix runs twice with a stable signature and observes every
    catalogued circuit; dummy-call padding remains intentionally deferred.
  • Claim-only shard preparation (no discarded dependency-byte closure walks)
    with a one-pass multi-root witness closure for proving.
  • ix aggregate --ixe E --ixes M [--structural-above N] [--direct-joins] [--jobs N] [--max-ram G] [--no-cache] [--plan-only] <proof>... and
    single-entrypoint aggregate verification with cryptographic-only,
    environment-bound, and manifest-bound value checks. The duplicate
    ix aggr command is removed.
  • Opt-in aggregate benchmarks: a tiny two-shard Init pair end-to-end bench and
    a flat-join bench (singleton CheckEnv shards → two lifts → verified flat
    join), with join metrics wired into reporting/dashboards, the pre-E2 lift
    proof size pinned at 7,986,166 bytes, and the post-E2 delta measured.
  • Positive, transitive-recursion, scheduler, cache, pruning, parity,
    malformed-input, and semantic-tampering tests: the aggregate-first suite
    has 58 cases, plus the separate activation matrix.

Protocol design

One recursion system, one entrypoint

Production builds Aggr.ixAggr, the shared Multi-STARK verifier closure pruned
to one ix_aggr entrypoint. A one-byte advice hint selects the exact verified
shape:

ShapeChildrenFold
0IxVMpass-through wrap
1ix_aggrpass-through wrap
2–5IxVM/ix_aggr pair (2 + 2·left + right)canonical flat fold
6–9IxVM/ix_aggr pair (6 + 2·left + right)structural fold

The hint is advice, not authority: each arm verifies the child proof(s) under
the key required by the encoded child kinds, then requires that kind's exact
claim layout. A wrong shape therefore fails proof verification, function-index
binding, or claim decoding.

Uniform public statement and pinned protocol identity

Every wrap and pair exposes the same public-input shape:

ix_aggr(allowed_digest: [G; 8], out_claim_digest: [G; 8])

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:

  1. the 80-byte allowed-system blob; and
  2. the serialized output CheckEnv claim.

The allowed blob is:

blake3(ixvm_vk)
|| verify_claim_idx:u64le
|| blake3(ix_aggr_vk)
|| ix_aggr_idx:u64le

The circuit requires exactly 80 bytes. 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
another function in those systems; binding the two accepted 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.
Every persisted aggregate outer claim is consequently the same 18-word form:
the ix_aggr function selector followed by the packed identity and output
claim digests. Shapes are witness choices and do not become root-kind metadata.

Child-proof validation and transitive pinning

Each pair reads the verifying key required by each child kind, checks its
Blake3 digest against the allowed blob, and deserializes it strictly. Wraps do
the same for their one child. For every child the selected arm:

  1. reads and fully consumes one proof and exactly one outer claim;
  2. runs the Multi-STARK structural verifier;
  3. runs ood_verify, including Fiat-Shamir replay, lookup accumulators,
    quotient/OOD checks, Merkle openings, and FRI verification; and
  4. decodes the statement according to the constrained child kind and function
    index.

The accepted forms are:

  • IxVM child: its system digest must equal blake3(ixvm_vk). Its claims
    digest opens to exactly one 10-word IxVM claim at the pinned
    verify_claim_idx, whose digest opens to a strict serialized CheckEnv
    claim.
  • ix_aggr child: its 18-word claim must use the pinned ix_aggr_idx; its
    first public digest must equal the current allowed_digest, transitively
    pinning both systems and both entrypoints; and its second digest opens to its
    output CheckEnv claim.

Any other index, shape, digest, trailing byte, or failed proof check rejects.
This keeps wrap→pair, flat→pair, and structural→structural composition on one
statement format.

Flat canonical folding

For child statements (S_L, A_L) and (S_R, A_R), flat shapes 2–5 enforce:

S_out = S_L ∪ S_R
A_out = (A_L ∪ A_R) ∖ S_out

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 u32 words using the
full-domain u32_less_than primitive. The implementation never uses Aiur
pointer 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

Structural shapes 6–9 perform the same allowed-blob binding, child-key
binding, two child-proof verifications, child decoding, and output-claim
binding as the flat shapes. They replace all subject-tree loading and set
re-rooting with:

S_out.root = nodeHash(S_L.root, S_R.root)

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:

  • carried (0): no bytes may follow, and the candidate must be the next
    value in the output assumption list; or
  • discharged (1): a bounded Merkle path from
    leafHash(candidate) to S_out.root must verify.

Path payloads are strict:

0
1 || count:u8 || repeated(side:u8 || sibling[32])

count is limited to 64. side = 0 hashes nodeHash(sibling, current) and
side = 1 hashes nodeHash(current, sibling); any other side rejects. The
payload 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

All ix_aggr shapes use the same seven-channel IO layout. Digest/root bindings
are checked before the corresponding bytes are decoded.

ChannelKeyValue
0[0], [1]first and optional second child proof advice bytes
1[kind]verifying-key bytes (0 = IxVM, 1 = ix_aggr)
2[0], [1], [2]left/right outer claims and output CheckEnv claim
3[0]80-byte allowed-system blob
4packed Blake3 digestnested claim preimages
5raw 32-byte addressserialized canonical trees keyed by root
6[0] / raw candidate addressshape byte / carried-or-discharged path choice

Lean passes channels 4–6 across FFI as compact framed blobs:

count:u32le || repeated(key[32] || payload_len:u32le || payload)

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.

Wraps receive no tree or path advice. Flat pairs receive full
subject/assumption tree advice and an empty path blob. Structural pairs receive
only the input/output assumption trees plus one path choice for every
deduplicated candidate; subject trees are never opened.

Manifest and host pipeline

.ixes aggregation tree

parseIxesManifest exposes both shard block lists and the optional binary
aggregation tree stored at the manifest tail. It:

  • requires nonzero, contiguous shard IDs;
  • validates option and tree tags;
  • requires tree leaves to be exactly the manifest shard set;
  • rejects duplicate/out-of-range leaves and trailing bytes; and
  • synthesizes a balanced ascending-ID tree for legacy manifests without a
    tree tail or with an explicit absent-tree tag.

The tree lowers to post-order FoldOp slots. Every join refers only to earlier
slots and the last slot is the root, providing a direct serial schedule and the
basis for cache keys and parallel execution.

Validate first, then prune empty manifest leaves

IxesManifestView retains the original shard id for every dense shard slot.
pruneEmpty:

  1. counts the environment constants owned by every raw shard;
  2. drops only shards with a count of zero;
  3. contracts aggregation-tree nodes with a removed child;
  4. densely remaps retained leaf indices; and
  5. returns the retained per-shard subject counts with the pruned view.

The aggregate and manifest-aware verifier paths deliberately call
shardsCover on the raw manifest before this transformation. Coverage
still establishes that every environment constant is owned exactly once. The
subsequent zero count is therefore evidence that a removed leaf contributes no
subject; pruning is not allowed to hide a missing or duplicated constant.

Original shard ids are preserved separately from dense scheduling ids, so CLI
plans and proof-binding errors continue to identify the source manifest shard.
An all-empty manifest is rejected because it cannot produce a root.

For a tree node node(left, right), pruning follows:

both retained -> node(left', right')
only left retained -> left'
only right retained -> right'
neither retained -> removed

The Rust .ixes writer (crates/kernel/src/shard.rs) also normalizes new
manifests before serialization — block-empty shard records are omitted, ids
rewritten densely, the tree pruned/contracted/remapped, and cross-ingress
recomputed. The Lean-side pass remains necessary for legacy manifests and for
the stronger environment-relative condition (a shard may contain blocks yet
own zero constants).

Monotone structural scheduling

The host counts actual owned constants for each shard after the disjoint-cover
check. schedulePlan annotates every slot with its cumulative subject count and
chooses a structural pair exactly when:

left.subjectCount + right.subjectCount > structuralAbove

The default threshold is 4096; --structural-above 0 makes every join
structural 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.

By default, every leaf is an ix_aggr shape-0 wrap, so binary slots are shape
5 below the threshold and shape 9 above it. With --direct-joins, a
multi-shard plan keeps leaves as raw IxVM children and derives each pair shape
from its actual child kinds: 2–5 for flat pairs or 6–9 for structural pairs.
The monotone threshold rule is unchanged.

The proving loop checks reconstructed subject counts against the schedule
before starting expensive work.

Singleton roots

ix aggregate imposes no two-shard minimum. The fold plan for one retained
leaf contains one shape-0 wrap and zero pairs; that wrapper and its CheckEnv
claim are persisted as the aggregate root. Even --direct-joins wraps a
singleton because the deliverable must verify under the one production
entrypoint. A self-pair would add work and manufacture a different statement.
Consequences:

  • exactly one proof is required per nonempty shard;
  • --plan-only reports one wrap and zero binary pairs for a singleton;
  • multi-shard flat/structural scheduling is unchanged; and
  • the final unconditional-root check always applies to an ix_aggr root.

ix aggregate

ix aggregate --ixe ENV.ixe --ixes SHARDS.ixes \ [--structural-above N] [--direct-joins] [--jobs N] [--max-ram G] [--no-cache] \ SHARD_PROOF_ADDR...

The command:

  1. deserializes the environment and manifest and verifies a disjoint exact
    shard cover on the raw manifest, then prunes zero-constant leaves;
  2. reconstructs every retained shard's exact CheckEnv claim and canonical
    trees (claim-only preparation — no dependency-byte closure);
  3. matches proof arguments to shards in any order by bundled claim digest,
    then requires exact claim equality and one proof per nonempty shard;
  4. natively verifies every IxVM shard proof before expensive recursion work;
  5. builds the IxVM and ix_aggr recursion systems and the pinned 80-byte
    allowed blob;
  6. derives every slot's statement, child kind, shape, uniform outer claim, and
    version-2 cache key up front;
  7. executes ready wraps and flat/structural pairs as a RAM-gated dependency
    DAG, resuming verified cached slots and persisting each completed wrapper
    (raw direct-policy leaves are verified inputs, not cache slots);
  8. canonicalizes all leaves under the final subject tree and checks that they
    reproduce the environment's canonical root;
  9. requires no remaining assumptions and natively verifies the outer proof at
    the one ix_aggr root entrypoint; and
  10. persists an Ixon.Proof containing the output CheckEnv claim and proof.

--plan-only stops after coverage validation and scheduling, resolving and
printing the scheduler policy without loading proofs or compiling recursion
systems:

ix aggregate --ixe ENV.ixe --ixes SHARDS.ixes \ --structural-above 0 --plan-only

ix verify --aggregate

Aggregate roots reuse Ixon.Proof, so verification is selected explicitly.
The verifier never accepts root-kind metadata. It serializes the bundled
CheckEnv value and reconstructs the only valid outer claim:

claim_bytes := serialize(CheckEnv(...))
outer_claim := ix_aggr(
blake3(allowed_blob),
blake3(claim_bytes))
InvocationCryptographic entrypointExpected statement value
ix verify --aggregate <proof>ix_aggrbundled claim; cryptographic proof only
ix verify --aggregate --ixe E <proof>ix_aggrcanonical environment root, unconditional
ix verify --aggregate --ixe E --ixes M <proof>ix_aggrpruned manifest fold, unconditional

Environment-only mode compares the bundled value to the canonical environment
root; a manifest-relative structural value therefore rejects naturally, with
no root-kind branch. Manifest-aware verification repeats pruning and the fold
schedule (the threshold must match the proving run), reconstructs every leaf
statement, folds the exact hybrid value, requires no assumptions, and verifies
the same ix_aggr outer claim. Existing non-aggregate proof and
shard-composition verification paths are unchanged.

Claim-only shard preparation

shardCheckEnvClaimTrees constructs only the CheckEnv claim and its
canonical subject/assumption trees. Aggregate planning, shard-digest
reconstruction, and aggregate verification use this path because none of them
consumes dependency bytes. shardCheckEnvClaim remains the witness-building
entry point; it reuses the same claim/tree result and computes the byte
closure once from the union of the owned and available primitive roots.
Reachability distributes over that root union, so this preserves the old
closure while avoiding repeated traversal of shared subgraphs.

Recursion parameters (WP-E1)

Aggregation and aggregate verification consume one shared
MultiStark.RecursionParameters value containing the recursion commitment and
FRI configurations, carried through runAggregateCmdWith/runVerifyCmdWith
into the shared MultiStark.buildRecursionSystem. The CLI entrypoints pass
defaultRecursionParameters, whose fields are exactly today's canonical Aiur
defaults. This is a mechanism split, not a policy change:

  • ordinary IxVM proving and verification remain on the canonical q=100
    configuration;
  • recursion proofs also remain q=100 with the same commitment and PoW values;
  • no new CLI flag selects security parameters; and
  • choosing q=50 plus a compensating PoW policy remains the explicit §9.5
    follow-up decision.

The recursion verifying key serializes and binds both parameter sets, and its
digest is included in every aggregate node's allowed-system blob, so any parameter
change changes the recursive protocol identity without a public-input or
allowed-blob format change. RecursionParameters.cacheFriBytes pins the
40-byte fri_params_ser cache-key component:

logFinalPolyLen:u64-le || maxLogArity:u64-le || numQueries:u64-le ||
commitProofOfWorkBits:u64-le || queryProofOfWorkBits:u64-le

Commitment parameters need no second cache component: changing them changes
the recursion-vk digest already present in the key.

Pruned FRI multiproofs (WP-E2)

The four-commit origin/update-multi-stark series is transplanted onto this
stack: Rust 1.98, the 249b7405 multi-stark revision on P3 v0.6, the matching
Blake3/Lean-FFI pins, and recursive proof-advice encoding. The new proof wire
format deduplicates shared Merkle siblings across sampled FRI queries. Width
and trace-height binding and the vk's checked max_multiplicity travel with
the dependency update; no aggregate claim or public-input format changes.

There are now deliberately two proof encodings. Proof.toBytes is the compact
wire form used by wrappers, native verification, the store, and WP-A cache
entries. Recursive execution still expects one path per query, so callers use
AiurSystem.proofToAdviceBytes immediately before a wrap or pair. The CLI,
pair/typecheck benchmarks, semantic tests, and activation audit all enforce
that boundary. Cached proofs stay compact at rest and are expanded only when
consumed as a parent pair's child.

The dependency's compact format is not backward-decodable. Store-facing CLI
verification therefore uses Proof.ofBytesChecked and reports a normal error
for legacy or corrupt proofs instead of crossing the unchecked decoder and
aborting. P3 v0.6 also requires a positive FRI query count; benchmark CLIs
reject --queries 0, and the old q=0 join smoke gate is replaced by q=1.
The Rust 1.98 migration also applies its new strict-Clippy equivalents for
fixed-size digest chunking and fallible metadata-size lookup; these are
mechanical, semantics-preserving rewrites.

Verified cache and resume (WP-A)

Before the first proof, the driver derives every fold slot's CheckEnv
statement, exact outer Aiur claim, and cache key, using the same
flat/structural fold policy as proving — so a manifest rebudget or statement
change invalidates only the changed subtree. The key:

blake3(
aggregateCacheVersion:u64-le ||
blake3(recursion_vk) ||
recursion_fri_params:40-bytes ||
serializeClaims([outer_claim])
)

The uniform outer claim already includes the ix_aggr entrypoint index and
public input, hence the allowed-system blob, output CheckEnv, both
verifying-key identities, and both accepted entrypoint indices. Each completed
recursive slot is stored as an Ixon.Proof wrapper in the content-addressed
~/.ix/store; a raw --direct-joins leaf is only an input and is not cached.
The wipeable index ~/.ix/cache/aggregate/<key> holds only the wrapper's store
address and is updated via temp-file + atomic rename. M1-d bumps
aggregateCacheVersion from 1 to 2, so old outer-claim entries miss cleanly.

A hit is an untrusted hint, not authority. Reuse requires all of: a
well-formed index address; store bytes whose Blake3 digest equals it; a
decodable wrapper; a bundled claim exactly equal to the precomputed slot
CheckEnv; a safely decoded Aiur proof (Aiur.Proof.ofBytesChecked — the
former trusted-byte constructor panicked on truncated bincode, and malformed
cache bytes must never abort the command); and native verification under the
exact precomputed outer claim and local recursion system. Any failure logs a
miss and re-proves. Cache write failures are warnings. On a valid hit the
host reconstructs the slot's preimages from the manifest statement, so
parents need no trusted metadata from the cache. --no-cache bypasses index
reads and intermediate writes while still persisting the final root wrapper.

RAM-gated parallel scheduling (WP-B)

The post-order fold executes as a dependency DAG: leaves are ready
immediately; a join becomes ready when both child slots have completed. The
controller sorts ready work by descending RAM weight (slot number breaks
ties) and starts one dedicated IO.asTask per admitted slot around the
existing proof FFI. Two independent limits govern admission:

  • --jobs N caps active slots; absent or 0 means every ready slot may run
    if it fits the budget;
  • --max-ram G caps total reserved GiB in flight, defaulting to 92% of Linux
    MemTotal.

As in the existing Rust RamGate, a slot whose estimate exceeds the entire
budget is admitted only while nothing else runs — a conservative estimate
serializes work rather than deadlocking. The calibration-pending per-shape
weights are:

raw IxVM leaf = 4 GiB
shape 0/1 wrap = 195 GiB
shape 2/6 IxVM + IxVM = 390 GiB
shape 3/4/7/8 mixed pair = 340 GiB
shape 5 self + self flat = 195 GiB + 1 MiB × subject leaves
shape 9 self + self struct = 195 GiB

The 195 GiB wrap reserve carries forward the recorded q=100 lift upper bound —
the measured peaks above (186.8–195.8 GiB) bracket it exactly. The 390/340 GiB
direct and mixed estimates deliberately keep the non-default policy out of the
256 GiB fleet tier pending M1-f calibration; measured self-pair peaks
(102.5–156.9 GiB) make their weights conservative in the safe direction. Real
calibration on a known-core box will tighten them without changing scheduler
semantics.

Results are installed by slot index, never arrival order; each worker gets an
immutable snapshot of its completed children, so parallel timing cannot
change parent advice. On failure the controller stops admitting, drains every
running proof, and reports the lowest failed slot; successful independent
tasks may finish publishing valid cache entries. Concurrent proofs share
Aiur's rayon global pool, so throughput can sub-scale even when memory
permits multiple slots; process-per-slot execution is a measurement-driven
follow-up.

Converged activation audit (WP-D + M1-e)

lake exe IxTests aggregate-activation is a dedicated diagnostic kept out of
the default suite. It executes the generated production ix_aggr path across
4 wraps and 128 pairs: two controlled child trace heights, shapes 0–9,
optional assumptions independently per side, complete discharge versus
explicit carry, and all four child-kind combinations in both flat and
structural modes. Every execution returns per-circuit (uniqueRows, totalHits); the runner performs the full 132-case matrix twice and rejects any
difference before rendering a deterministic per-shape Markdown report.
Current signature:

263a3087d42340613b012d50a9416e23549f85aa1ea425e86bf0a216c7b3e27d

Result: 183 audited circuits — 154 active in every case, 29 input-dependent,
zero never observed. The variable set concentrates in the expected paths:
wrap dispatch, canonical set folding, structural Merkle discharge, and
child-kind decoding. Fixed-height Bytes1/Bytes2 circuits are
outside the execute FFI's query-count array and are called out explicitly.
The audit records information only; dummy calls would add cost for the
currently planned SP1 terminal and should be chosen only if a future static
terminal requires input-independent activation.

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.executeMultiStarkJoin for generated or interpreted flat
    and structural join execution;
  • AiurSystem.proveMultiStarkJoin for witness generation and proving;
  • JoinAdvice, JoinPreimage, JoinTree, JoinPath, strict framed decoders,
    and the seven-channel join_io_buffer;
  • the matching rs_aiur_multi_stark_join_execute and
    rs_aiur_multi_stark_join_prove externs; and
  • Aiur.Proof.ofBytesChecked for panic-free store-boundary proof decoding.

The retained three-entrypoint executor is parity-checked against the bytecode
interpreter on both output and every circuit's unique-row/total-hit counts. The
combined verifier contains 247 Aiur functions; the regenerated Rust file is
2,239,894 bytes (ixvm: 5,807,375 bytes / 780 functions). Most line churn is
generated code. The protocol source of truth is
Ix/MultiStark/Aggregate.lean; the native advice contract lives in
aiur_multi_stark_runner.rs. It is now an Ix-agnostic regression/reference backend,
not the production CLI backend.

The production ix_aggr executor is independently parity-checked for wrap,
flat, and structural shapes. It contains 248 Aiur functions and its generated
Rust file is 2,061,361 bytes. Its source of truth is
Ix/Aggr/Circuit.lean, with native advice construction in
aiur_ix_aggr_runner.rs; all three generated artifacts are checked with
ix codegen --check.

Benchmarks

M1-f replaces the legacy two-shard/three-entrypoint runner with
bench-aggregate-policy, a production ix_aggr benchmark for one exact
four-shard fixture. It selects the four retained leaves from a validated and
pruned manifest, contracts the surrounding tree without changing orientation,
and independently reconstructs the expected host root. Wrap-first uses shape 0
leaves; direct mode keeps IxVM leaves raw. Both policies then select their flat
or structural heterogeneous shapes from the production plan. Measured runs
require q=100, jobs=1, --no-cache, and JSON output; the harness natively
verifies all four inputs and every recursive output, persists every completed
slot as an ordinary Ixon.Proof, and writes resumable per-slot and whole-run
timing/RSS/proof metadata.

The existing bench-typecheck --recursive --join row keeps its stable
join-* reporting/dashboard schema, but its join phase now exercises direct
ix_aggr shape 2 instead of the retired join_two circuit. Historical size
baselines remain recorded: 7,986,166 bytes (7,986,204 with the Ixon.Proof
wrapper) for a one-constant q=100 lift under the pre-E2 247-function system,
versus 7,443,023 bytes (7,443,061-byte wrapper) after WP-E2, a 543,143-byte /
6.80% reduction. Pruned multiproof length depends on sampled query-path
overlap, so a positive-PoW output is now a sample rather than an exact
byte-length pin. The deterministic q=100/PoW-0 regression signal is 7,447,279
bytes and 38,850,942,825 recursive-verifier FFT cost. The target-box policy
comparison is the remaining measurement, not remaining harness work.

Test coverage

The legacy aggregate-first suite uses a small stand-in child system so it can
create real Multi-STARK child proofs without the tens to hundreds of GiB
required by a full recursion proof. Those proofs are consumed by the retained
join circuits, so proof validation, statement decoding, set/path folding,
native advice, and transitive recursion are exercised end-to-end at execution
time.

The 57 passing legacy aggregate-first cases cover:

  • canonical flat and structural host folds; manifest parsing, validation, and
    post-order lowering;
  • distinct lift/flat/structural compiled entrypoint identities;
  • honest flat union and cross-child discharge; structural path discharge plus
    a carried assumption;
  • flat-join and structural-join outer claim layout and native verification;
  • transitively pinned flat and structural children; rejection of a child
    carrying a different allowed digest;
  • generated/interpreter parity for both join modes; threshold scheduling that
    is flat below and structural above monotonically;
  • strict keyed-blob framing; wrong-root and tampered-sibling paths; a missing
    path choice; a carried candidate omitted from the output; the obsolete
    88-byte allowed blob; a flat join fed a genuinely free-form structural
    child root; omitted/extra assumptions or subjects; noncanonical unsorted
    trees; tampered child proofs;
  • a synthetic three-leaf manifest with empty outer leaves: raw coverage before
    pruning, contraction, dense remapping, and singleton-lift classification;
  • claim-only shard preparation preserving exact subject/assumption trees,
    with the one-pass witness closure matching the legacy per-owned-root union;
  • exact legacy verifier reconstruction of a lift outer claim and native
    verification under it;
  • byte-for-byte equality of the shared default recursion builder with the
    former direct construction; the exact 40-byte FRI cache encoding;
    independent recursion-vk identity changes for FRI and commitment overrides;
  • cache-key invalidation across version, recursion vk, FRI parameters, and
    outer claim; whole-plan statement/claim/key derivation before proving;
    atomic index creation, corrupt-index detection and repair; a
    content-addressed, claim-bound, natively verified resume hit; mismatched
    claim rejection; corrupt store-content fallback; checked rejection of
    malformed proof bytes without a Rust panic;
  • heaviest-first admission with deterministic tie-breaking; job and byte
    ceilings; dependency release; admit-alone oversize handling; the flat-join
    affine weight and MemTotal parser; jobs=1/jobs=2 payload equality;
    failure draining without admitting a dependent join; jobs=1/jobs=2
    byte-identical recursive wrappers under canonical zero-PoW grinding, with
    each scheduled child decoded and verified; and
  • Rust writer normalization plus round-tripping a genuinely noncanonical
    structural CheckEnv root.

The wrapper-byte scheduler gate uses zero PoW deliberately: with the pinned
positive-PoW implementation, rayon's find_any selects any passing grind
witness, so two valid serial proofs already differ in bytes. Production
jobs=1/jobs=2 correctness is the stronger protocol invariant — exact claims
and native verification under the same recursion system. The primitive
Multi-STARK and recursive-verifier suites were rerun to guard the shared
verifier and lift paths, and the separate activation gate covers 132 accepted
shape/input cases twice.

The focused ix-aggr command now runs 93 passing checks. Its 42
circuit/shape/plan checks cover both wraps, all flat and structural child-kind
pairs, a nested structural self child, generated/interpreter parity, strict
tree and native keyed-blob framing, identity/shape closure, wrong or tampered
paths, omitted carries, unsorted trees, flat↔structural hint confusion, a flat
parent fed a free-form structural root, and exact four-shard wrap-first/direct
plans. Its 51 converged semantic checks cover uniform claims, cache v2 key
invalidation and verified resume, recursion parameters, manifest
validation/pruning, singleton/canonical/hybrid value reconstruction,
shard-preparation closure sharing, wrap-first/direct planning, per-shape RAM
admission, dependency/failure behavior, and jobs=1/jobs=2 determinism including
concurrent zero-PoW proving. Rust unit tests separately pin channel-6 key layout
and strict path-blob framing.

Validation run

All of the following pass on the current stack:

nix develop --command lake exe IxTests aggregate-first # 57/57nix develop --command lake exe IxTests ix-aggr # 93/93nix develop --command lake exe IxTests aggregate-activation # 132 cases × 2 stable passesnix develop --command lake exe IxTests multi-stark # 12/12nix develop --command lake exe IxTests recursive-verifier # 6/6nix develop --command lake build IxTests bench-typecheck bench-recursion-debug bench-aggregate-policy ixnix develop --command cargo test -p ixvm-codegen aiur_multi_stark_runnernix develop --command cargo test -p ixvm-codegen aiur_ix_aggr_runnernix develop --command cargo test -p ix-kernel shard::tests # 23/23nix develop --command cargo test -p ixon test_check_env_claim_accepts_structural_rootnix develop --command cargo clippy -p ixvm-codegen -p ix-ffi -p ix-kernel -p ixon --tests -- -D warningsnix develop --command cargo fmt --all -- --checknix develop --command lake exe ix codegen --checkgit diff --check

Both benchmark entrypoints also reject --queries 0 with a usage error before
building or proving, and a stored pre-E2 proof now reports a checked decode
error rather than aborting. The fresh post-E2 aggregate fixture verifies
against both its .ixe and .ixes inputs.

A q=1 live smoke of the converged typecheck join also executed, proved, and
verified direct shape 2 successfully: 1.221742 s execution, 26,738,665,748 FFT
cost, 52.380108 s proving, 21,565,714,432-byte peak RSS, 401,340-byte proof,
and 0.002616 s verification. The M1-f policy harness passed plan-only smoke for
both policies and emitted the expected resumable JSON schema.

Pre-convergence real-environment gates on the 124 GB development host (kept
as WP-A/WP-B baselines for the M1-f rerun):

  • A two-shard manifest from the 5,986-constant tc-parity.ixe environment
    passes coverage and structural scheduling with threshold zero
    (2 lifts + 1 binary joins (1 structural)), and its one-shard variant
    plans 1 lifts + 0 binary joins.
  • Before E2, a one-constant q=100 production lift ran end-to-end through the
    cache and scheduler, establishing WP-A's invalidation/repair baseline:
PassResultWall
Initialproved and cached root 090bea6f…ca1535~64 s
Identical rerunverified cache hit, same root1.66 s
Truncated indexclean miss, re-proof, atomic repair, same root~38 s
Post-repair rerunverified cache hit, same root1.79 s
--jobs 2 --max-ram 400195 GiB reserved, verified hit, same root1.45 s

At scale, the measured pre-E2 4-shard Init run in Measured performance
above exercised the former backend with real shard proofs, four q=100 lifts,
two lower joins, and a root join with genuine assumption discharge. The pinned
M1-f harness will repeat this fixture under both converged policies.

Soundness summary

  • Every present child is fully verified under the verifying key selected by
    its shape-constrained kind.
  • Child function indices are constrained to IxVM verify_claim or ix_aggr,
    and every recursive child must carry the same allowed digest, pinning the
    two verifying keys and two entrypoints transitively.
  • Claims and allowed data are Blake3-bound and strictly parsed with no trailing
    bytes.
  • Flat mode reopens canonical roots and proves exact union/difference;
    structural mode proves the exact root-of-roots relation and accounts for
    every unique input assumption through either inclusion or explicit carry.
  • A bogus path, dropped candidate, extra survivor, malformed choice, or
    noncanonical assumption tree rejects. Over-carrying and duplicate subject
    leaves can only weaken the output claim; the CLI requires an unconditional
    final root.
  • Coverage is checked on the unmodified manifest, so pruning cannot conceal a
    hole or overlap; only leaves with an environment-derived owned-constant
    count of zero are removed, and contraction preserves the order and shape of
    retained leaves.
  • A singleton root is a verified shape-0 wrapper, never a raw-proof shortcut;
    the circuit binds its output digest directly to the pinned IxVM child's
    CheckEnv digest.
  • Manifest-aware verification independently repeats pruning, scheduling, and
    value folding, then verifies the one derived ix_aggr outer claim. No
    claimed proof kind or shape is accepted from wrapper metadata.
  • Proving and verification build the recursion system from the same explicit
    parameter object; any parameter change necessarily changes the recursion vk,
    allowed-system identity, and cache key.
  • Cache keys bind version, recursion-vk digest, explicit FRI bytes, and exact
    outer claim, all derived before proving; hits re-hash store content, bind
    the claim, decode without panicking, and verify natively. Corrupt, missing,
    or stale entries are misses, never trusted successes or command failures.
  • A pair is admitted only after both child slots complete; immutable
    snapshots and indexed installation make arrival order irrelevant. The
    controller owns all reservations; failures drain running work before exit.
  • Free-form structural roots are serialized as opaque commitments
    (crates/kernel/src/claim.rs); canonical openings are required only by
    protocol paths that explicitly need them.

Review map

Commit-by-commit review is recommended — each commit is feature-scoped with
its own tests (map in the first comment / commit list).

AreaPrimary filesWhat to review
Legacy regression protocolIx/MultiStark/Aggregate.lean, Ix/MultiStark.leanpre-convergence 96-byte identity and semantic/audit reference surface
Production circuitIx/Aggr/Circuit.lean, Ix/Aggr.lean80-byte identity, uniform claims, shapes 0–9, heterogeneous child pinning, structural path checks
Converged host modelIx/Aggr/Host.leanfree-form structural roots, canonical survivors, path extraction
Host statement modelIx/MultiStark/Host.leanfree-form subject roots, canonical assumptions, path extraction
Manifest parsing + pruningIx/Cli/CheckCmd.leanstrict tree parsing, validate-before-prune, unary contraction, dense remap
Aggregate driverIx/Cli/AggregateCmd.leanwrap-first/direct shape policy, singleton wrap, uniform slot claims, cache v2, per-shape RAM weights, verified DAG resume/failure draining
Root verificationIx/Cli/VerifyCmd.leanone ix_aggr outer claim, value-based environment comparison, manifest-relative hybrid reconstruction
Recursion parametersIx/MultiStark.lean, Ix/Aggr.lean, Ix/Cli/{Aggregate,Verify}Cmd.leanshared config plumbing, deterministic systems, stable FRI cache encoding
Pruned FRI multiproofsIx/Aiur/{Protocol,Semantics/BytecodeFfi}.lean, Ix/Cli/{Aggregate,Verify}Cmd.lean, crates/ffi/src/aiur/protocol.rscompact persisted proof vs expanded recursive advice boundary, checked legacy-proof failure
Cache/store boundaryIx/Store.lean, Ix/Aiur/Protocol.lean, crates/ffi/src/aiur/protocol.rswipeable namespace, checked proof decoding, corrupt-data fallthrough
Shard preparationIx/IxVM/ClaimHarness.lean, Ix/Cli/{Aggregate,Check,Verify}Cmd.leanclaim/tree-only consumers, one-pass multi-root witness closure
Native advicecrates/ixvm-codegen/src/aiur_multi_stark_runner.rsstrict framing and seven-channel key layout
Converged native advicecrates/ixvm-codegen/src/aiur_ix_aggr_runner.rs, crates/ffi/src/aiur/protocol.rsstrict path framing, shared channel-6 key layout, execute/prove passthrough
Generated executorcrates/ixvm-codegen/src/aiur_multi_stark.rsgenerated artifact; verify with ix codegen --check
Converged generated executorcrates/ixvm-codegen/src/aiur_ix_aggr.rsgenerated artifact; verify with ix codegen --check
Manifest writercrates/kernel/src/shard.rsproducer-side omission, tree remap, aggregate metadata
Claim semanticscrates/kernel/src/claim.rs, crates/ixon/src/proof.rsfree-form root contract and codec regression test
TestsTests/Aggr.lean, Tests/AggrSemantics.lean, Tests/AggrActivation.lean, Tests/MultiStark.lean93 production shape/semantic/plan checks, 132-case activation audit, retained Ix-agnostic regressions
BenchmarksBenchmarks/AggregatePolicy.lean, Benchmarks/Typecheck.lean, bench registry/dashboardspinned four-shard wrap-first/direct handoff, resumable JSON, converged direct-shape-2 join metrics, historical lift-size pins

Current limits and non-goals

  • Recursion FRI parameters stay at the q=100 defaults; the q=50 + compensating
    PoW policy is an explicit follow-up decision (the measured costs above are
    therefore the conservative end).
  • Structural roots are manifest-relative. Environment-only verification
    accepts only a bundled value equal to the canonical environment root;
    hybrid roots should be verified with both --ixe and --ixes and the
    proving threshold.
  • Scheduler RAM weights are calibration-pending placeholders (validated in
    the safe direction by the measured run); concurrent proves share the rayon
    global pool, so throughput can sub-scale — process-per-slot is a
    measurement-driven follow-up.
  • Same-key cross-process cache writes degrade to a warning and recomputation,
    never a soundness issue.
  • Dummy-call activation padding is deferred until/unless a static terminal
    circuit is selected; under the SP1 terminal it is pure cost.
  • Aggregate wrappers reuse Ixon.Proof and do not encode the proof system;
    callers must pass ix verify --aggregate.
  • M1-a–f land the converged production circuit, host/native substrate, driver,
    cache, scheduler, value-based verifier, semantic union, widened activation
    audit, and policy benchmark harness. The only remaining M1 work is executing
    the wrap-first/direct comparison on the target box. The pinned M1-f revision,
    inherited M1-e preflight, and both measured policies are one checkout/session;
    its handoff is documented in
    plans/aggregate-first-m1f-large-box-runbook.md.
  • This PR does not adopt cold-circuit grouping and does not implement a
    terminal KZG/SP1 wrapper.

Follow-ups

  1. Calibrate lift/join unit costs on a known-core box, with per-node
    flat-vs-structural attribution, and measure frontier/discharge-path counts
    at Mathlib scale (plan §11.4.1).
  2. Decide the recursion q=50 + PoW policy (§9.5) now that the mechanism
    exists.
  3. Connect the root proof to the selected terminal compression path (E3:
    single-recursion-proof SP1 guest + ix compress-root).
  4. Re-evaluate cold-circuit grouping (E4); E2's measured outer-lift reduction
    was only 6.80%, so grouping may still be material.
  5. Process-per-slot proving if measured rayon contention warrants it.
  6. Run the exact M1-f commit
    b70042d6d1a029bb8e1d541da7aef5e0613d67e6 for the combined M1-e preflight
    and wrap-first/direct policy benchmark session in the large-box handoff
    runbook.

johnchandlerburnhamand others added 22 commits August 27, 2026 16:21
Add the deterministic lift/flat/structural activation matrix and keep dummy calls deferred. Split aggregate recursion commitment/FRI configuration from IxVM defaults, share it between proving and verification, and pin the future cache encoding without changing active protocol parameters.
Precompute versioned per-slot cache keys, persist lift and join wrappers in the content-addressed store, and reuse entries only after exact claim and native outer-proof verification. Add safe proof decoding, corruption recovery, and --no-cache.
Execute ready lift and join slots as a dependency DAG under explicit job and RAM admission. Add calibration-pending slot weights, failure draining, CLI controls, and serial/parallel scheduler and proof-equivalence gates.
Add an opt-in two-child benchmark that proves singleton CheckEnv shards, lifts both proofs, and measures a verified flat join. Wire join metrics through reporting and dashboards, and record the current pre-E2 lift-size baseline.
- rust-toolchain.toml channel: 1.92 → 1.98, with the matching fenix
toolchain hash in flake.nix. The pinned fenix already carries the 1.98
release manifest, so flake.lock needs no change (fenix's nixpkgs stays
pinned via its lean4-nix follows, so the Lean toolchain is untouched).
- Drop clippy::from_iter_instead_of_collect from the workspace lints:
removed in clippy 1.98 and now warns as unknown.
- Fix the warnings new clippy 1.98 lints surface across the workspace:
chunks_exact(N) → as_chunks::<N>() where the chunk size is constant,
descending sort_by → sort_by_key(Reverse(..)), iteration over map
values via .values(), map().unwrap_or() → map_or(), a checked
division, an unwrap-after-is_some restructured into if-let, and
assorted redundant-reference/pattern cleanups (mostly cargo clippy
--fix). The two byte-gadget files keep their chunks_exact warnings
until the next commit, which rewrites those regions anyway.
CI derives its Rust version from rust-toolchain.toml, so no workflow
changes are needed.
Companion to multi-stark's update-p3 branch (c72d321 → 249b740), which
carries four soundness/robustness fixes and the Plonky3 v0.6.0 bump
(pruned FRI Merkle multiproofs: ~2x faster verification, 40-70% smaller
proofs; canonical Goldilocks serde removes proof-byte malleability).
Proofs and verifying keys are not compatible with the previous pin.
Integration:
- Lookup gained max_multiplicity, a declared per-row bound on the
multiplicity's integer magnitude feeding the newly enforced logUp
height bound Σ wᵢ·hᵢ + |claims| < p. Function-circuit slots accumulate
mutually-exclusive branch selectors, so they declare 1; committed
count columns (function return slots, the memory circuit, the byte
gadget tables) declare the new COUNT_COLUMN_BUDGET (2^32 queries per
entry).
- The VK wire format carries the bound: u64 LE max_multiplicity per
lookup, between the multiplicity node id and the arg count. The
in-circuit VK deserializer (Ix/MultiStark/SystemDeserialize.lean)
parses past it; the value is bound through the vk digest but the
height bound itself is not yet enforced in-circuit (the native
verifier enforces it).
- Message fingerprints are width-bound by default upstream (the slot
width seeds the Horner fold), which is incompatible with aiur's
branch-shared lookup slots: mutually exclusive branches superpose
messages of different natural widths into one slot at the maximum
width, so a narrow call is sent zero-padded to a width its callee's
return slot never provides, and proving fails with
UnbalancedChannel (pinned as the prove_verify_mismatched_call_widths
regression). Aiur instead declares WidthBinding::ByConstruction —
the plain Horner fold, restoring zero-padding transparency — and
takes on the prefix-freeness contract that makes it sound: every
message's natural width is a function of its constant-constrained
leading prefix (channel tag plus discriminator: fun_idx fixes
2+in+out, the memory size coordinate fixes 3+size, each gadget tag
fixes its table width), so zero-extension can only equate a padded
message with its own natural form. The contract is documented at the
channel constants in lib.rs; the declaration is applied in
AiurSystem::build and mirrored in the vk_codec decoder so decoded
VKs replay the same transcript. ByConstruction is also exactly the
fold the in-circuit verifier's logup_fingerprint already computes,
so the recursive verifier needs no fingerprint change.
- The policy is Fiat-Shamir-bound as the first observe_shape word; the
in-circuit transcript replay prepends the matching limb.
- aiur_multi_stark.rs regenerated (ix codegen) for the deserializer
and transcript changes.
- P3 v0.6.0 ships FRI query openings as pruned Merkle multiproofs,
while the in-circuit verifier consumes one authentication path per
query (its per-query control flow is a far smaller circuit than the
amortized multiproof walk). Rather than porting the walk into the
DSL, the proof advice stays in the per-query transport: multi-stark's
new advice module re-encodes a natively-verified proof by running
p3's own verification with a recording compression function and
reading each query's path back out of the recorded digest map. The
advice bytes are untrusted verifier input, never digest-bound —
the transcript binds the commitments and every expanded sibling is
authenticated against them per query — so pruning vs expansion is
pure transport and the encoding choice is sound. AiurSystem gains
proof_to_advice_bytes (FFI: AiurSystem.proofToAdviceBytes); the
recursive-verifier test feeds it instead of Proof.toBytes, whose
native wire format is still round-tripped separately. The Lean-side
proof grammar and the codegen'd verifier are byte-identical to
before — no in-circuit changes.
Claim layout, the VK wire format above and aiur's public semantics are
otherwise unchanged; the policy adds no prover or verifier work over
the previous pin.
Still open, native-verifier-only: the logUp height bound is parsed
past but not yet enforced in-circuit (a wide-arithmetic check, tracked
separately).
The Lean v4.33.1 update pinned both dependencies at revisions that
predate their Rust 1.98 bumps; their heads now carry those bumps, which
this workspace needs since rust-toolchain.toml moved to 1.98. Both
revisions stay on leanprover/lean4:v4.33.1.
- Blake3.lean 1b0fbd2 → e6e908b (Rust 1.98, plus a case-insensitive
source-directory fix), updated in lakefile.lean, lake-manifest.json
and the blake3-lean flake input. The revision keeps the
`blake3_rs_shared` target the `ix_native_decide_dynlib` pin requires.
The inherited entry in Benchmarks/Compile/lake-manifest.json was still
on the pre-4.33.1 revision and now tracks the root pin.
- lean-ffi 2a9c91e → 93c7e52 (Rust 1.98). Only bignat reaches the sp1
and zisk workspaces, so their lock files move that one package.
The !benchmark recursive phase reported n/a for every fri-verifier
metric: Benchmarks/Typecheck.lean still fed Proof.toBytes — the pruned
multiproof wire format — to executeMultiStark/proveMultiStark, so the
in-circuit verifier rejected on parse and the harness (correctly) left
the recursive fields absent rather than emit a fake datum. The
in-circuit verifier consumes the per-query advice transport
(AiurSystem.proofToAdviceBytes); proofBytes stays the reported
proof-size metric.
bench-recursion-debug had the same advice-format gap plus a stale
claim recipe: it still built the public input as 32 raw digest bytes,
predating the ClaimHarness.packedDigestKey packing bench-typecheck
uses (its own out-of-circuit sanity check failed with
InvalidPowWitness — a wrong claim diverges every challenge — and the
advice re-encoder refused the proof for the same reason). Both aligned
with the typecheck flow.
Validated end-to-end at production parameters (numQueries 100,
query PoW 20, blowup 2) on Nat.add_comm: inner prove, advice
re-encoding, and the codegen'd in-circuit verifier accepting.
Keep compact proof bytes at storage and cache boundaries, expand them only for recursive lift/join advice, reject unsupported zero-query benchmarks, and decode legacy store proofs without panicking. Refresh tests, benchmark pins, and Rust 1.98 lint compatibility.
Add Ix/Aggr, a recursive aggregation system for IxVM shard proofs that
keeps Ix/MultiStark untouched and Ix-agnostic. One entrypoint, ix_aggr,
subsumes lifting and joining: a one-byte advice shape selects wrap or
binary join over any mix of IxVM and ix_aggr children, so shard proofs
enter the recursion system directly as join children and the dedicated
lift stage disappears.
Circuit (Ix/Aggr/Circuit.lean): every shape verifies its children in
full (verify + ood_verify from the shared Multi-STARK verifier modules)
against the vk its hinted kind demands. Identity is one 80-byte
digest-bound blob - blake3(ixvm vk) || verify_claim idx ||
blake3(self vk) || ix_aggr idx - carried unchanged at every node; self
children must bind the identical blob digest, pinning both vks and both
entrypoint indices transitively. Claims are a uniform 18-word
[0, aggr_idx, allowed(8), checkEnv(8)] at every depth, so proofs of
different tree levels combine freely. Wrap shapes bind the output
digest to the child CheckEnv digest directly; pair shapes open both
CheckEnv preimages, re-root the canonical subject/assumption trees, and
prove subjects = L ∪ R, assumptions = (asmL ∪ asmR) ∖ subjects with
linear sorted merges.
Toplevel (Ix/Aggr.lean): ixAggr = MultiStark.multiStarkFull + circuit,
pruned to ix_aggr, so verify_multi_stark_proof and other unrelated
entries no longer pad aggregate proofs. The host half of the wire
contracts (allowed blob, public input packing, shape codes, keyed
preimage/tree blob framing, interpreter IO assembly) lives beside the
toplevel; Ix/Aggr/Host.lean folds CheckEnvTrees statements.
Native path: ix codegen gains the ix-aggr target
(crates/ixvm-codegen/src/aiur_ix_aggr.rs, 244 fns); its runner builds
the seven-channel IO buffer natively and routes execution through the
generated code. New FFI rs_aiur_ix_aggr_execute/_prove
(executeIxAggr/proveIxAggr) pass proofs, vks, claims, and the compact
count/key/length preimage/tree blobs without per-byte boxing, plus
Proof.ofBytesChecked for store-boundary decoding.
CLI: ix aggr --ixe E --ixes M <shard-proof>... reconstructs every
nonempty shard statement from the env, matches wrappers by claim
digest, natively pre-verifies them, folds a balanced bisection (the
canonical fold makes the root claim independent of tree shape), wraps
single-shard roots so the persisted root is always an ix_aggr proof,
checks the root closes over the env canonical tree with no residual
assumptions, and persists the wrapper.
Tests (lake test -- ix-aggr, 17 cases): all five shapes accept over
real Multi-STARK stand-in child proofs from two distinct-vk systems;
codegen'd execution matches the interpreter on output and per-circuit
query counts for wrap and pair; negatives break one binding each -
lying shape hint, tampered proof, foreign identity, wrap statement
drift, dropped assumption, padded subject set, and tree advice not
reproducing its keyed root.
Replace the legacy lift/join entrypoints with the heterogeneous ix_aggr system across aggregate, verification, cache, and codegen paths. Preserve wrap-first and direct-join policies behind one proof identity and cache namespace.
Add the converged 91-check semantic suite and a deterministic 132-case activation audit covering shapes 0 through 9 twice, with no unobserved circuits.
Replace the legacy two-shard three-entrypoint benchmark with a four-shard manifest-subtree harness for production ix_aggr wrap-first and direct policies. Enforce the q=100 serialized no-cache profile, natively verify every input and recursive output, persist ordinary aggregate wrappers, and emit resumable per-slot JSON metrics.
Port bench-typecheck --join to ix_aggr direct shape 2 while preserving the stable join metric schema. Remove the final legacy slot-spec/preimage shim and pin both M1-f four-shard plans in the focused suite.
Update the multi-stark dependency and Rust toolchain for Plonky3 0.6, along with the Rust 1.98 lint migrations required to keep the workspace warning-free. Refresh the Rust-compatible Blake3.lean pin in both root and compile-package manifests.
Adapt recursive Aiur verification to Plonky3's pruned FRI multiproofs. Native proofs retain their compact serialized representation and native verification path; the FFI expands authenticated Merkle frontiers into per-query advice only when entering the existing recursive verifier circuit.
Preserve the packed claim-digest convention in the recursion diagnostic and exercise the advice boundary in the end-to-end test and benchmark paths. CPU and CUDA recursive q1 runs produce identical 823,485-byte inner proofs and 331,273-byte outer proofs.
The q50 Vector.extract_append workload retains identical CPU/CUDA proof sizes. Inner plus outer STARK proving measures 65.87s on CPU and 8.81s with CUDA on the RTX PRO 6000, a 7.48x speedup.
PR benchmark runs execute trusted workflow YAML from the default branch while loading composite actions from the PR checkout. When Bencher data and binary caches are unavailable, the workflow checks out main under base/ and asks Lake to rebuild it without first installing the Rust channel pinned by that checkout.
Teach the existing CPU provenance action to install the base checkout's validated Rust channel and profile immediately before an uncached base build. The step is a no-op when the toolchain is already available and leaves cached benchmark comparisons unchanged.
Consume the Plonky3 0.6 batch-opening layout directly in Aiur instead of expanding every pruned Merkle frontier into one authentication path per FRI query. Sample all query indices from the unchanged transcript, sort and deduplicate them with an O(q log q) merge sort, authenticate each input and commit-phase commitment once, then retain the existing per-query reduced-opening and FRI arithmetic.
Bind every frontier to transcript-derived indices, consume boundary digests in Plonky3's level/parent/child order, reject trailing frontier elements and inconsistent duplicate leaves, and assert all native opening dimensions and sibling counts. Explicitly constrain the digest-bound protocol specialization to cap height 0, binary FRI, and a constant final polynomial. Move memo_u32_less_than into IxVM Core so both substitution and multiproof sorting share its constrained rows.
Strengthen the recursive negative test to mutate a structurally valid stage-1 commitment. Regenerate both checked-in Aiur Rust executors and retain interpreter/codegen query-count parity.
On Vector.extract_append q50, recursive-verifier FFT cost falls from 204.073B to 201.166B. CPU outer proving improves from 50.09s to 45.03s and the full CPU pipeline from 90.64s to 82.90s. GPU outer proving improves from 15.85s to 13.72s and the full GPU pipeline from 28.86s to 26.69s. The outer proof grows from 3.92 MB to 4.17 MB.
Validated with the MultiStark primitive suite, recursive honest/tamper/parity tests, codegen --check, release workspace clippy, release CUDA clippy, rustfmt, and diff checks.
@johnchandlerburnham

Copy link
Copy Markdown
MemberAuthor

Init full proving report

Revision: 82dcc9dba36d0cfd6c753c6878ab649643a52026
Host: 64 vCPU, 495 GiB RAM, 256 GiB swap
Final result: one verified ix_aggr FRI proof with no remaining assumptions.

Compilation and sharding

StepResult
Compile sourceBenchmarks/Compile/CompileInit.lean
Ix compiler time1.876s
Constants compiled65,994
init.ixe size194,296,853 bytes (185.3 MiB)
Static sharding time3.1s
Manifest16 nonempty shards
Retained/owned constants56,621
Manifest size2,619,404 bytes (2.50 MiB)
Predicted FFT per shard4.160e11–4.197e11
Predicted FFT spread1.01×

The 1.876-second compile measurement is the Ix compiler itself; it excludes the initial one-time dependency download and Lake build.

Stage 1: base shard proofs

All 16 shard proofs were persisted and individually verified against their corresponding manifest claims.

MetricSerial baselineTwo-way run used for aggregation
Concurrency12
End-to-end wall time24m 16.76s15m 53.93s
Sum of shard prover times22m 43.23s30m 08.14s
Per-shard time range74.08–113.97s98.94–142.29s
Median shard time82.66s108.75s
Highest individual RSS219.8 GiB219.8 GiB
Combined process-tree peak358.2 GiB
Proof count1616
Total proof-wrapper size369,995,038 bytes
Mean proof-wrapper size23,124,690 bytes
Proof-wrapper range22,714,520–23,998,692 bytes

Two-way concurrency gave a 1.53× wall-time speedup despite increasing total prover-seconds by about 33% from CPU/memory contention.

Stage 2: recursive aggregation

Policy:

  • Wrap-first
  • --structural-above 4096
  • --jobs 2
  • --max-ram 450
  • Default q=100 recursion parameters
  • Aggregate cache enabled

Plan:

OperationCount
Shard wraps16
Binary joins15
Structural joins14
Flat joins1
Total recursive slots31

Results:

MetricResult
Wall time45m 06.61s
Peak resident memory379.9 GiB
Average CPU utilization4,192% (~42 cores)
Cache hits0
Freshly proved/cached slots31
Swap used0 bytes
Exit status0
Final proof-wrapper size9,596,044 bytes (9.15 MiB)

Final proof

Root address:

80f6844f968920abaef8d8c750a1a1dc1ebe32657a3cfb824e1810bcd65c8814

Final claim:

CheckEnv(6dc8f821a989c6f7b9d520b053ef8edbd582d2408d7b6a6ff72b908666e03b6f, none)

Both validations passed:

  • Cryptographic-only aggregate verification
  • Manifest-aware verification against init.ixe, init-16.ixes, and structural threshold 4096

End-to-end measured time

Using the two-way Stage 1 run:

ComponentTime
Ix compilation1.876s
Sharding3.1s
Stage 1 proofs15m 53.932s
Stage 2 aggregation45m 06.61s
Measured core pipeline1h 01m 05.52s

This total excludes the one-time dependency build/download and a few seconds of manifest and final-root verification overhead. Using serial Stage 1 instead would make the measured core pipeline approximately 1h 09m 28.35s.

Use the PR77 lean-ffi and multi-stark pins, remove the stale width-binding and multiplicity-bound protocol fields, and regenerate the recursive and aggregate verifier sources.
Expose an opt-in x86_64-linux cuda shell with NVCC 13.2, sm_120 code generation, and safe host libcuda discovery while leaving the default development shell unchanged.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@johnchandlerburnham@samuelburnham@arthurpaulino
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root - #598

Open
johnchandlerburnham wants to merge 24 commits into
mainfrom
jcb/aggregate-first
Open

Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root#598
johnchandlerburnham wants to merge 24 commits into
mainfrom
jcb/aggregate-first

Conversation

@johnchandlerburnham

@johnchandlerburnhamjohnchandlerburnham commented Aug 27, 2026

Copy link
Copy Markdown
Member

Summary

This PR implements the aggregate-first path for Aiur Multi-STARK proofs. The
production path now runs through one ix_aggr recursion entrypoint: it wraps
independently-proven IxVM shards, folds their CheckEnv statements along the
manifest's bisection tree, and persists one recursive root proof with a
uniform 18-word outer claim. Every completed recursive slot is persisted in a
verified, content-addressed version-2 resume cache, and ready slots are proven
in parallel under explicit job and RAM limits.

The entrypoint has two pair-fold modes:

  • Flat shapes perform a canonical set union/difference and are efficient for
    small lower nodes.
  • Structural shapes commit to nodeHash(leftRoot, rightRoot) in one hash and
    discharge assumptions with Merkle inclusion paths. Their 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 (default
4096 subject leaves): flat below the threshold, structural above it.

IxVM shard proofs
│
├── ix_aggr shape 0 (default wrap-first policy)
│ or remain raw with --direct-joins
▼
ix_aggr proofs carrying uniform CheckEnv statements
│
├── flat ix_aggr shapes 2–5 below the configured subject threshold
│ subjects := canonical(subjectsL ∪ subjectsR)
│ assumptions := (assumptionsL ∪ assumptionsR) ∖ subjects
│
└── structural ix_aggr shapes 6–9 above the threshold
subjects := nodeHash(subjectRootL, subjectRootR)
assumptions := candidates carried or discharged by inclusion path
▼
Ixon.Proof {
claim := CheckEnv(manifest-relative subject root, none),
proof := one recursive Multi-STARK root proof
}

A manifest with one retained (nonempty) shard is also a valid deliverable: its
root is one shape-0 wrapper, with no self-join padding (including under
--direct-joins). Zero-constant manifest leaves are pruned after raw coverage
validation:

raw manifest
|
| validate exact environment coverage before pruning
v
drop zero-constant leaves + contract unary tree nodes + remap retained ids
|
+-- one retained leaf --> wrap once and persist the ix_aggr root
|
`-- multiple leaves --> fold flat/structural joins as above

This is the stage-2 aggregation layer from
plans/aggregate-first-pipeline.md: shard proofs are aggregated before any
terminal KZG/SP1 compression, so a later wrapper only needs to consume one root
proof. Terminal compression remains outside this PR.

Section 14 implementation is complete through M1-f in this branch. The
ap/ix-aggr single-entrypoint base is reconciled with WP-E2's compact stored
proof / expanded recursive-advice boundary, and its one ix_aggr entrypoint
supports wraps, flat pairs, and structural pairs across both IxVM and recursive
children. ix aggregate now uses that backend for planning, proving, cache
resume, and final verification; ix verify --aggregate derives one uniform
outer claim and performs value-based environment/manifest checks. The old
ix aggr command and all three-entrypoint production wiring are retired. The
older Multi-STARK aggregate implementation remains as an Ix-agnostic regression
surface, not as a CLI backend. Production semantics, activation coverage, and
benchmarking now live entirely against ix_aggr. M1-f lands the pinned
four-shard bench-aggregate-policy handoff and ports the stable typecheck join
metric to direct shape 2; only execution of the policy comparison on the large
box remains. Operationally this is one future handoff: Arthur will check out
b70042d6d1a029bb8e1d541da7aef5e0613d67e6 once, run the inherited M1-e
correctness suite as untimed preflight, and then benchmark both policies. There
is no separate M1-e box run.

Measured performance (2026-08-29)

Arthur Paulino ran the pre-convergence pipeline end-to-end on four real Init
shard proofs (shards 8–11 of a 16-way partition, 12,493 constants; base shard
proving excluded; native verification of each output included; ~512 GB box).
These are the standing M1-f comparison baseline; the converged wrap-first and
direct policies have not yet been rerun on that box. The recursion used the
conservative q=100 / PoW 20 defaults this PR keeps:

StageTimePeak RAMOutput proof
Lift × 498.8–103.0 s (mean ≈ 101 s)186.8–195.8 GiB8,162,462 B each
Join 8+9, 10+1148.9 / 48.5 s~102.5 GiB9,502,843 B
Root join85.8 s156.9 GiB9,455,498 B
Total (serialized)10:23 wall195.8 GiB9,455,498 B

Notes: lift cost is shard-content-independent (four lifts within ±2%, tracking
the ~constant 20–23 MB shard-proof shape); the measured lift peak brackets the
scheduler's 195 GiB weight placeholder exactly; and an unserialized run of all
four lifts OOM'd at ~492 GiB — precisely the behavior the WP-B admission gate
prevents (on a 512 GB box its weights admit two lifts). Because shards 8–11
are a strict partition subset, the root carries a real frontier assumption
set, so the joins exercised genuine discharge work. Full analysis, including
the comparison against direct (unlifted) IxVM joins, is in
plans/aggregate-first-pipeline.md §3.4 and the Zulip thread.

What lands

Protocol / circuit layer:

  • One production recursion entrypoint, ix_aggr, under one verifying key.
    Shapes 0–1 wrap one IxVM/recursive child, shapes 2–5 fold flat pairs, and
    shapes 6–9 fold structural pairs for every child-kind combination.
  • Recursive validation of wrap, flat-pair, and structural-pair children with
    transitive verifying-key, function-index, and allowed-system binding. A
    shape hint selects the verified form but cannot weaken its checks.
  • Strict in-circuit CheckEnv decoding and canonical flat set folding.
  • Structural subject folding with per-candidate Merkle discharge paths while
    keeping output assumption sets canonical and deduplicated.
  • One 80-byte protocol identity containing the IxVM/self verifying-key
    digests and their two accepted entrypoint indices; every recursive node has
    the same 18-word outer-claim layout regardless of shape.
  • Structural outputs bind nodeHash(leftRoot, rightRoot) and account for each
    unique assumption candidate through a strict Merkle-path-or-carry choice.
  • Host helpers construct structural roots, survivors, and path advice. Channel
    6 carries both the one-byte shape at key [0] and candidate path payloads at
    raw 32-byte address keys; the key shapes are disjoint.
  • Lean/Rust execute and prove FFI accept a compact keyed path blob, whose
    framing is decoded strictly before the circuit independently validates
    payload semantics.
  • The generated ix_aggr executor is regenerated at 2,061,361 bytes / 248
    Aiur functions and parity-tested against the interpreter.

Host driver and operations:

  • Strict .ixes aggregation-tree parsing and post-order lowering, with a
    balanced fallback for legacy manifests; validate-then-prune removal of
    zero-constant leaves with unary tree contraction (and a Rust writer that
    stops emitting them).
  • Single-shard roots: a one-retained-shard manifest always produces one
    shape-0 ix_aggr wrapper, including under --direct-joins.
  • A monotone leaf-count scheduler controlled by --structural-above, with a
    default wrap-first plan (shapes 0 then 5/9) and an explicit
    --direct-joins plan that derives heterogeneous shapes from actual child
    kinds.
  • Versioned, content-addressed aggregate cache and resume (WP-A): version 2
    binds the uniform outer claim, and reuse occurs only after content-digest,
    claim, decode, and native verification all pass; --no-cache bypasses.
  • Parallel execution of ready wraps/pairs as a dependency DAG under --jobs
    and --max-ram (default 92% of MemTotal), heaviest-first, with failure
    draining (WP-B). Admission weights are shape-aware: 4 GiB for a retained
    raw IxVM leaf, 195 GiB for wraps/self pairs, 390 GiB for IxVM/IxVM pairs,
    and 340 GiB for mixed pairs.
  • A recursion-parameter split (WP-E1): aggregate proving/verification share an
    explicit RecursionParameters value, defaulting to today's q=100 values;
    the q=50+PoW policy decision is deliberately deferred.
  • A deterministic 132-case production activation audit across shapes 0–9:
    two controlled child trace heights, both wraps, every flat/structural
    child-kind pair, optional assumptions per side, and discharge/carry. The
    complete matrix runs twice with a stable signature and observes every
    catalogued circuit; dummy-call padding remains intentionally deferred.
  • Claim-only shard preparation (no discarded dependency-byte closure walks)
    with a one-pass multi-root witness closure for proving.
  • ix aggregate --ixe E --ixes M [--structural-above N] [--direct-joins] [--jobs N] [--max-ram G] [--no-cache] [--plan-only] <proof>... and
    single-entrypoint aggregate verification with cryptographic-only,
    environment-bound, and manifest-bound value checks. The duplicate
    ix aggr command is removed.
  • Opt-in aggregate benchmarks: a tiny two-shard Init pair end-to-end bench and
    a flat-join bench (singleton CheckEnv shards → two lifts → verified flat
    join), with join metrics wired into reporting/dashboards, the pre-E2 lift
    proof size pinned at 7,986,166 bytes, and the post-E2 delta measured.
  • Positive, transitive-recursion, scheduler, cache, pruning, parity,
    malformed-input, and semantic-tampering tests: the aggregate-first suite
    has 58 cases, plus the separate activation matrix.

Protocol design

One recursion system, one entrypoint

Production builds Aggr.ixAggr, the shared Multi-STARK verifier closure pruned
to one ix_aggr entrypoint. A one-byte advice hint selects the exact verified
shape:

ShapeChildrenFold
0IxVMpass-through wrap
1ix_aggrpass-through wrap
2–5IxVM/ix_aggr pair (2 + 2·left + right)canonical flat fold
6–9IxVM/ix_aggr pair (6 + 2·left + right)structural fold

The hint is advice, not authority: each arm verifies the child proof(s) under
the key required by the encoded child kinds, then requires that kind's exact
claim layout. A wrong shape therefore fails proof verification, function-index
binding, or claim decoding.

Uniform public statement and pinned protocol identity

Every wrap and pair exposes the same public-input shape:

ix_aggr(allowed_digest: [G; 8], out_claim_digest: [G; 8])

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:

  1. the 80-byte allowed-system blob; and
  2. the serialized output CheckEnv claim.

The allowed blob is:

blake3(ixvm_vk)
|| verify_claim_idx:u64le
|| blake3(ix_aggr_vk)
|| ix_aggr_idx:u64le

The circuit requires exactly 80 bytes. 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
another function in those systems; binding the two accepted 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.
Every persisted aggregate outer claim is consequently the same 18-word form:
the ix_aggr function selector followed by the packed identity and output
claim digests. Shapes are witness choices and do not become root-kind metadata.

Child-proof validation and transitive pinning

Each pair reads the verifying key required by each child kind, checks its
Blake3 digest against the allowed blob, and deserializes it strictly. Wraps do
the same for their one child. For every child the selected arm:

  1. reads and fully consumes one proof and exactly one outer claim;
  2. runs the Multi-STARK structural verifier;
  3. runs ood_verify, including Fiat-Shamir replay, lookup accumulators,
    quotient/OOD checks, Merkle openings, and FRI verification; and
  4. decodes the statement according to the constrained child kind and function
    index.

The accepted forms are:

  • IxVM child: its system digest must equal blake3(ixvm_vk). Its claims
    digest opens to exactly one 10-word IxVM claim at the pinned
    verify_claim_idx, whose digest opens to a strict serialized CheckEnv
    claim.
  • ix_aggr child: its 18-word claim must use the pinned ix_aggr_idx; its
    first public digest must equal the current allowed_digest, transitively
    pinning both systems and both entrypoints; and its second digest opens to its
    output CheckEnv claim.

Any other index, shape, digest, trailing byte, or failed proof check rejects.
This keeps wrap→pair, flat→pair, and structural→structural composition on one
statement format.

Flat canonical folding

For child statements (S_L, A_L) and (S_R, A_R), flat shapes 2–5 enforce:

S_out = S_L ∪ S_R
A_out = (A_L ∪ A_R) ∖ S_out

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 u32 words using the
full-domain u32_less_than primitive. The implementation never uses Aiur
pointer 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

Structural shapes 6–9 perform the same allowed-blob binding, child-key
binding, two child-proof verifications, child decoding, and output-claim
binding as the flat shapes. They replace all subject-tree loading and set
re-rooting with:

S_out.root = nodeHash(S_L.root, S_R.root)

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:

  • carried (0): no bytes may follow, and the candidate must be the next
    value in the output assumption list; or
  • discharged (1): a bounded Merkle path from
    leafHash(candidate) to S_out.root must verify.

Path payloads are strict:

0
1 || count:u8 || repeated(side:u8 || sibling[32])

count is limited to 64. side = 0 hashes nodeHash(sibling, current) and
side = 1 hashes nodeHash(current, sibling); any other side rejects. The
payload 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

All ix_aggr shapes use the same seven-channel IO layout. Digest/root bindings
are checked before the corresponding bytes are decoded.

ChannelKeyValue
0[0], [1]first and optional second child proof advice bytes
1[kind]verifying-key bytes (0 = IxVM, 1 = ix_aggr)
2[0], [1], [2]left/right outer claims and output CheckEnv claim
3[0]80-byte allowed-system blob
4packed Blake3 digestnested claim preimages
5raw 32-byte addressserialized canonical trees keyed by root
6[0] / raw candidate addressshape byte / carried-or-discharged path choice

Lean passes channels 4–6 across FFI as compact framed blobs:

count:u32le || repeated(key[32] || payload_len:u32le || payload)

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.

Wraps receive no tree or path advice. Flat pairs receive full
subject/assumption tree advice and an empty path blob. Structural pairs receive
only the input/output assumption trees plus one path choice for every
deduplicated candidate; subject trees are never opened.

Manifest and host pipeline

.ixes aggregation tree

parseIxesManifest exposes both shard block lists and the optional binary
aggregation tree stored at the manifest tail. It:

  • requires nonzero, contiguous shard IDs;
  • validates option and tree tags;
  • requires tree leaves to be exactly the manifest shard set;
  • rejects duplicate/out-of-range leaves and trailing bytes; and
  • synthesizes a balanced ascending-ID tree for legacy manifests without a
    tree tail or with an explicit absent-tree tag.

The tree lowers to post-order FoldOp slots. Every join refers only to earlier
slots and the last slot is the root, providing a direct serial schedule and the
basis for cache keys and parallel execution.

Validate first, then prune empty manifest leaves

IxesManifestView retains the original shard id for every dense shard slot.
pruneEmpty:

  1. counts the environment constants owned by every raw shard;
  2. drops only shards with a count of zero;
  3. contracts aggregation-tree nodes with a removed child;
  4. densely remaps retained leaf indices; and
  5. returns the retained per-shard subject counts with the pruned view.

The aggregate and manifest-aware verifier paths deliberately call
shardsCover on the raw manifest before this transformation. Coverage
still establishes that every environment constant is owned exactly once. The
subsequent zero count is therefore evidence that a removed leaf contributes no
subject; pruning is not allowed to hide a missing or duplicated constant.

Original shard ids are preserved separately from dense scheduling ids, so CLI
plans and proof-binding errors continue to identify the source manifest shard.
An all-empty manifest is rejected because it cannot produce a root.

For a tree node node(left, right), pruning follows:

both retained -> node(left', right')
only left retained -> left'
only right retained -> right'
neither retained -> removed

The Rust .ixes writer (crates/kernel/src/shard.rs) also normalizes new
manifests before serialization — block-empty shard records are omitted, ids
rewritten densely, the tree pruned/contracted/remapped, and cross-ingress
recomputed. The Lean-side pass remains necessary for legacy manifests and for
the stronger environment-relative condition (a shard may contain blocks yet
own zero constants).

Monotone structural scheduling

The host counts actual owned constants for each shard after the disjoint-cover
check. schedulePlan annotates every slot with its cumulative subject count and
chooses a structural pair exactly when:

left.subjectCount + right.subjectCount > structuralAbove

The default threshold is 4096; --structural-above 0 makes every join
structural 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.

By default, every leaf is an ix_aggr shape-0 wrap, so binary slots are shape
5 below the threshold and shape 9 above it. With --direct-joins, a
multi-shard plan keeps leaves as raw IxVM children and derives each pair shape
from its actual child kinds: 2–5 for flat pairs or 6–9 for structural pairs.
The monotone threshold rule is unchanged.

The proving loop checks reconstructed subject counts against the schedule
before starting expensive work.

Singleton roots

ix aggregate imposes no two-shard minimum. The fold plan for one retained
leaf contains one shape-0 wrap and zero pairs; that wrapper and its CheckEnv
claim are persisted as the aggregate root. Even --direct-joins wraps a
singleton because the deliverable must verify under the one production
entrypoint. A self-pair would add work and manufacture a different statement.
Consequences:

  • exactly one proof is required per nonempty shard;
  • --plan-only reports one wrap and zero binary pairs for a singleton;
  • multi-shard flat/structural scheduling is unchanged; and
  • the final unconditional-root check always applies to an ix_aggr root.

ix aggregate

ix aggregate --ixe ENV.ixe --ixes SHARDS.ixes \ [--structural-above N] [--direct-joins] [--jobs N] [--max-ram G] [--no-cache] \ SHARD_PROOF_ADDR...

The command:

  1. deserializes the environment and manifest and verifies a disjoint exact
    shard cover on the raw manifest, then prunes zero-constant leaves;
  2. reconstructs every retained shard's exact CheckEnv claim and canonical
    trees (claim-only preparation — no dependency-byte closure);
  3. matches proof arguments to shards in any order by bundled claim digest,
    then requires exact claim equality and one proof per nonempty shard;
  4. natively verifies every IxVM shard proof before expensive recursion work;
  5. builds the IxVM and ix_aggr recursion systems and the pinned 80-byte
    allowed blob;
  6. derives every slot's statement, child kind, shape, uniform outer claim, and
    version-2 cache key up front;
  7. executes ready wraps and flat/structural pairs as a RAM-gated dependency
    DAG, resuming verified cached slots and persisting each completed wrapper
    (raw direct-policy leaves are verified inputs, not cache slots);
  8. canonicalizes all leaves under the final subject tree and checks that they
    reproduce the environment's canonical root;
  9. requires no remaining assumptions and natively verifies the outer proof at
    the one ix_aggr root entrypoint; and
  10. persists an Ixon.Proof containing the output CheckEnv claim and proof.

--plan-only stops after coverage validation and scheduling, resolving and
printing the scheduler policy without loading proofs or compiling recursion
systems:

ix aggregate --ixe ENV.ixe --ixes SHARDS.ixes \ --structural-above 0 --plan-only

ix verify --aggregate

Aggregate roots reuse Ixon.Proof, so verification is selected explicitly.
The verifier never accepts root-kind metadata. It serializes the bundled
CheckEnv value and reconstructs the only valid outer claim:

claim_bytes := serialize(CheckEnv(...))
outer_claim := ix_aggr(
blake3(allowed_blob),
blake3(claim_bytes))
InvocationCryptographic entrypointExpected statement value
ix verify --aggregate <proof>ix_aggrbundled claim; cryptographic proof only
ix verify --aggregate --ixe E <proof>ix_aggrcanonical environment root, unconditional
ix verify --aggregate --ixe E --ixes M <proof>ix_aggrpruned manifest fold, unconditional

Environment-only mode compares the bundled value to the canonical environment
root; a manifest-relative structural value therefore rejects naturally, with
no root-kind branch. Manifest-aware verification repeats pruning and the fold
schedule (the threshold must match the proving run), reconstructs every leaf
statement, folds the exact hybrid value, requires no assumptions, and verifies
the same ix_aggr outer claim. Existing non-aggregate proof and
shard-composition verification paths are unchanged.

Claim-only shard preparation

shardCheckEnvClaimTrees constructs only the CheckEnv claim and its
canonical subject/assumption trees. Aggregate planning, shard-digest
reconstruction, and aggregate verification use this path because none of them
consumes dependency bytes. shardCheckEnvClaim remains the witness-building
entry point; it reuses the same claim/tree result and computes the byte
closure once from the union of the owned and available primitive roots.
Reachability distributes over that root union, so this preserves the old
closure while avoiding repeated traversal of shared subgraphs.

Recursion parameters (WP-E1)

Aggregation and aggregate verification consume one shared
MultiStark.RecursionParameters value containing the recursion commitment and
FRI configurations, carried through runAggregateCmdWith/runVerifyCmdWith
into the shared MultiStark.buildRecursionSystem. The CLI entrypoints pass
defaultRecursionParameters, whose fields are exactly today's canonical Aiur
defaults. This is a mechanism split, not a policy change:

  • ordinary IxVM proving and verification remain on the canonical q=100
    configuration;
  • recursion proofs also remain q=100 with the same commitment and PoW values;
  • no new CLI flag selects security parameters; and
  • choosing q=50 plus a compensating PoW policy remains the explicit §9.5
    follow-up decision.

The recursion verifying key serializes and binds both parameter sets, and its
digest is included in every aggregate node's allowed-system blob, so any parameter
change changes the recursive protocol identity without a public-input or
allowed-blob format change. RecursionParameters.cacheFriBytes pins the
40-byte fri_params_ser cache-key component:

logFinalPolyLen:u64-le || maxLogArity:u64-le || numQueries:u64-le ||
commitProofOfWorkBits:u64-le || queryProofOfWorkBits:u64-le

Commitment parameters need no second cache component: changing them changes
the recursion-vk digest already present in the key.

Pruned FRI multiproofs (WP-E2)

The four-commit origin/update-multi-stark series is transplanted onto this
stack: Rust 1.98, the 249b7405 multi-stark revision on P3 v0.6, the matching
Blake3/Lean-FFI pins, and recursive proof-advice encoding. The new proof wire
format deduplicates shared Merkle siblings across sampled FRI queries. Width
and trace-height binding and the vk's checked max_multiplicity travel with
the dependency update; no aggregate claim or public-input format changes.

There are now deliberately two proof encodings. Proof.toBytes is the compact
wire form used by wrappers, native verification, the store, and WP-A cache
entries. Recursive execution still expects one path per query, so callers use
AiurSystem.proofToAdviceBytes immediately before a wrap or pair. The CLI,
pair/typecheck benchmarks, semantic tests, and activation audit all enforce
that boundary. Cached proofs stay compact at rest and are expanded only when
consumed as a parent pair's child.

The dependency's compact format is not backward-decodable. Store-facing CLI
verification therefore uses Proof.ofBytesChecked and reports a normal error
for legacy or corrupt proofs instead of crossing the unchecked decoder and
aborting. P3 v0.6 also requires a positive FRI query count; benchmark CLIs
reject --queries 0, and the old q=0 join smoke gate is replaced by q=1.
The Rust 1.98 migration also applies its new strict-Clippy equivalents for
fixed-size digest chunking and fallible metadata-size lookup; these are
mechanical, semantics-preserving rewrites.

Verified cache and resume (WP-A)

Before the first proof, the driver derives every fold slot's CheckEnv
statement, exact outer Aiur claim, and cache key, using the same
flat/structural fold policy as proving — so a manifest rebudget or statement
change invalidates only the changed subtree. The key:

blake3(
aggregateCacheVersion:u64-le ||
blake3(recursion_vk) ||
recursion_fri_params:40-bytes ||
serializeClaims([outer_claim])
)

The uniform outer claim already includes the ix_aggr entrypoint index and
public input, hence the allowed-system blob, output CheckEnv, both
verifying-key identities, and both accepted entrypoint indices. Each completed
recursive slot is stored as an Ixon.Proof wrapper in the content-addressed
~/.ix/store; a raw --direct-joins leaf is only an input and is not cached.
The wipeable index ~/.ix/cache/aggregate/<key> holds only the wrapper's store
address and is updated via temp-file + atomic rename. M1-d bumps
aggregateCacheVersion from 1 to 2, so old outer-claim entries miss cleanly.

A hit is an untrusted hint, not authority. Reuse requires all of: a
well-formed index address; store bytes whose Blake3 digest equals it; a
decodable wrapper; a bundled claim exactly equal to the precomputed slot
CheckEnv; a safely decoded Aiur proof (Aiur.Proof.ofBytesChecked — the
former trusted-byte constructor panicked on truncated bincode, and malformed
cache bytes must never abort the command); and native verification under the
exact precomputed outer claim and local recursion system. Any failure logs a
miss and re-proves. Cache write failures are warnings. On a valid hit the
host reconstructs the slot's preimages from the manifest statement, so
parents need no trusted metadata from the cache. --no-cache bypasses index
reads and intermediate writes while still persisting the final root wrapper.

RAM-gated parallel scheduling (WP-B)

The post-order fold executes as a dependency DAG: leaves are ready
immediately; a join becomes ready when both child slots have completed. The
controller sorts ready work by descending RAM weight (slot number breaks
ties) and starts one dedicated IO.asTask per admitted slot around the
existing proof FFI. Two independent limits govern admission:

  • --jobs N caps active slots; absent or 0 means every ready slot may run
    if it fits the budget;
  • --max-ram G caps total reserved GiB in flight, defaulting to 92% of Linux
    MemTotal.

As in the existing Rust RamGate, a slot whose estimate exceeds the entire
budget is admitted only while nothing else runs — a conservative estimate
serializes work rather than deadlocking. The calibration-pending per-shape
weights are:

raw IxVM leaf = 4 GiB
shape 0/1 wrap = 195 GiB
shape 2/6 IxVM + IxVM = 390 GiB
shape 3/4/7/8 mixed pair = 340 GiB
shape 5 self + self flat = 195 GiB + 1 MiB × subject leaves
shape 9 self + self struct = 195 GiB

The 195 GiB wrap reserve carries forward the recorded q=100 lift upper bound —
the measured peaks above (186.8–195.8 GiB) bracket it exactly. The 390/340 GiB
direct and mixed estimates deliberately keep the non-default policy out of the
256 GiB fleet tier pending M1-f calibration; measured self-pair peaks
(102.5–156.9 GiB) make their weights conservative in the safe direction. Real
calibration on a known-core box will tighten them without changing scheduler
semantics.

Results are installed by slot index, never arrival order; each worker gets an
immutable snapshot of its completed children, so parallel timing cannot
change parent advice. On failure the controller stops admitting, drains every
running proof, and reports the lowest failed slot; successful independent
tasks may finish publishing valid cache entries. Concurrent proofs share
Aiur's rayon global pool, so throughput can sub-scale even when memory
permits multiple slots; process-per-slot execution is a measurement-driven
follow-up.

Converged activation audit (WP-D + M1-e)

lake exe IxTests aggregate-activation is a dedicated diagnostic kept out of
the default suite. It executes the generated production ix_aggr path across
4 wraps and 128 pairs: two controlled child trace heights, shapes 0–9,
optional assumptions independently per side, complete discharge versus
explicit carry, and all four child-kind combinations in both flat and
structural modes. Every execution returns per-circuit (uniqueRows, totalHits); the runner performs the full 132-case matrix twice and rejects any
difference before rendering a deterministic per-shape Markdown report.
Current signature:

263a3087d42340613b012d50a9416e23549f85aa1ea425e86bf0a216c7b3e27d

Result: 183 audited circuits — 154 active in every case, 29 input-dependent,
zero never observed. The variable set concentrates in the expected paths:
wrap dispatch, canonical set folding, structural Merkle discharge, and
child-kind decoding. Fixed-height Bytes1/Bytes2 circuits are
outside the execute FFI's query-count array and are called out explicitly.
The audit records information only; dummy calls would add cost for the
currently planned SP1 terminal and should be chosen only if a future static
terminal requires input-independent activation.

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.executeMultiStarkJoin for generated or interpreted flat
    and structural join execution;
  • AiurSystem.proveMultiStarkJoin for witness generation and proving;
  • JoinAdvice, JoinPreimage, JoinTree, JoinPath, strict framed decoders,
    and the seven-channel join_io_buffer;
  • the matching rs_aiur_multi_stark_join_execute and
    rs_aiur_multi_stark_join_prove externs; and
  • Aiur.Proof.ofBytesChecked for panic-free store-boundary proof decoding.

The retained three-entrypoint executor is parity-checked against the bytecode
interpreter on both output and every circuit's unique-row/total-hit counts. The
combined verifier contains 247 Aiur functions; the regenerated Rust file is
2,239,894 bytes (ixvm: 5,807,375 bytes / 780 functions). Most line churn is
generated code. The protocol source of truth is
Ix/MultiStark/Aggregate.lean; the native advice contract lives in
aiur_multi_stark_runner.rs. It is now an Ix-agnostic regression/reference backend,
not the production CLI backend.

The production ix_aggr executor is independently parity-checked for wrap,
flat, and structural shapes. It contains 248 Aiur functions and its generated
Rust file is 2,061,361 bytes. Its source of truth is
Ix/Aggr/Circuit.lean, with native advice construction in
aiur_ix_aggr_runner.rs; all three generated artifacts are checked with
ix codegen --check.

Benchmarks

M1-f replaces the legacy two-shard/three-entrypoint runner with
bench-aggregate-policy, a production ix_aggr benchmark for one exact
four-shard fixture. It selects the four retained leaves from a validated and
pruned manifest, contracts the surrounding tree without changing orientation,
and independently reconstructs the expected host root. Wrap-first uses shape 0
leaves; direct mode keeps IxVM leaves raw. Both policies then select their flat
or structural heterogeneous shapes from the production plan. Measured runs
require q=100, jobs=1, --no-cache, and JSON output; the harness natively
verifies all four inputs and every recursive output, persists every completed
slot as an ordinary Ixon.Proof, and writes resumable per-slot and whole-run
timing/RSS/proof metadata.

The existing bench-typecheck --recursive --join row keeps its stable
join-* reporting/dashboard schema, but its join phase now exercises direct
ix_aggr shape 2 instead of the retired join_two circuit. Historical size
baselines remain recorded: 7,986,166 bytes (7,986,204 with the Ixon.Proof
wrapper) for a one-constant q=100 lift under the pre-E2 247-function system,
versus 7,443,023 bytes (7,443,061-byte wrapper) after WP-E2, a 543,143-byte /
6.80% reduction. Pruned multiproof length depends on sampled query-path
overlap, so a positive-PoW output is now a sample rather than an exact
byte-length pin. The deterministic q=100/PoW-0 regression signal is 7,447,279
bytes and 38,850,942,825 recursive-verifier FFT cost. The target-box policy
comparison is the remaining measurement, not remaining harness work.

Test coverage

The legacy aggregate-first suite uses a small stand-in child system so it can
create real Multi-STARK child proofs without the tens to hundreds of GiB
required by a full recursion proof. Those proofs are consumed by the retained
join circuits, so proof validation, statement decoding, set/path folding,
native advice, and transitive recursion are exercised end-to-end at execution
time.

The 57 passing legacy aggregate-first cases cover:

  • canonical flat and structural host folds; manifest parsing, validation, and
    post-order lowering;
  • distinct lift/flat/structural compiled entrypoint identities;
  • honest flat union and cross-child discharge; structural path discharge plus
    a carried assumption;
  • flat-join and structural-join outer claim layout and native verification;
  • transitively pinned flat and structural children; rejection of a child
    carrying a different allowed digest;
  • generated/interpreter parity for both join modes; threshold scheduling that
    is flat below and structural above monotonically;
  • strict keyed-blob framing; wrong-root and tampered-sibling paths; a missing
    path choice; a carried candidate omitted from the output; the obsolete
    88-byte allowed blob; a flat join fed a genuinely free-form structural
    child root; omitted/extra assumptions or subjects; noncanonical unsorted
    trees; tampered child proofs;
  • a synthetic three-leaf manifest with empty outer leaves: raw coverage before
    pruning, contraction, dense remapping, and singleton-lift classification;
  • claim-only shard preparation preserving exact subject/assumption trees,
    with the one-pass witness closure matching the legacy per-owned-root union;
  • exact legacy verifier reconstruction of a lift outer claim and native
    verification under it;
  • byte-for-byte equality of the shared default recursion builder with the
    former direct construction; the exact 40-byte FRI cache encoding;
    independent recursion-vk identity changes for FRI and commitment overrides;
  • cache-key invalidation across version, recursion vk, FRI parameters, and
    outer claim; whole-plan statement/claim/key derivation before proving;
    atomic index creation, corrupt-index detection and repair; a
    content-addressed, claim-bound, natively verified resume hit; mismatched
    claim rejection; corrupt store-content fallback; checked rejection of
    malformed proof bytes without a Rust panic;
  • heaviest-first admission with deterministic tie-breaking; job and byte
    ceilings; dependency release; admit-alone oversize handling; the flat-join
    affine weight and MemTotal parser; jobs=1/jobs=2 payload equality;
    failure draining without admitting a dependent join; jobs=1/jobs=2
    byte-identical recursive wrappers under canonical zero-PoW grinding, with
    each scheduled child decoded and verified; and
  • Rust writer normalization plus round-tripping a genuinely noncanonical
    structural CheckEnv root.

The wrapper-byte scheduler gate uses zero PoW deliberately: with the pinned
positive-PoW implementation, rayon's find_any selects any passing grind
witness, so two valid serial proofs already differ in bytes. Production
jobs=1/jobs=2 correctness is the stronger protocol invariant — exact claims
and native verification under the same recursion system. The primitive
Multi-STARK and recursive-verifier suites were rerun to guard the shared
verifier and lift paths, and the separate activation gate covers 132 accepted
shape/input cases twice.

The focused ix-aggr command now runs 93 passing checks. Its 42
circuit/shape/plan checks cover both wraps, all flat and structural child-kind
pairs, a nested structural self child, generated/interpreter parity, strict
tree and native keyed-blob framing, identity/shape closure, wrong or tampered
paths, omitted carries, unsorted trees, flat↔structural hint confusion, a flat
parent fed a free-form structural root, and exact four-shard wrap-first/direct
plans. Its 51 converged semantic checks cover uniform claims, cache v2 key
invalidation and verified resume, recursion parameters, manifest
validation/pruning, singleton/canonical/hybrid value reconstruction,
shard-preparation closure sharing, wrap-first/direct planning, per-shape RAM
admission, dependency/failure behavior, and jobs=1/jobs=2 determinism including
concurrent zero-PoW proving. Rust unit tests separately pin channel-6 key layout
and strict path-blob framing.

Validation run

All of the following pass on the current stack:

nix develop --command lake exe IxTests aggregate-first # 57/57nix develop --command lake exe IxTests ix-aggr # 93/93nix develop --command lake exe IxTests aggregate-activation # 132 cases × 2 stable passesnix develop --command lake exe IxTests multi-stark # 12/12nix develop --command lake exe IxTests recursive-verifier # 6/6nix develop --command lake build IxTests bench-typecheck bench-recursion-debug bench-aggregate-policy ixnix develop --command cargo test -p ixvm-codegen aiur_multi_stark_runnernix develop --command cargo test -p ixvm-codegen aiur_ix_aggr_runnernix develop --command cargo test -p ix-kernel shard::tests # 23/23nix develop --command cargo test -p ixon test_check_env_claim_accepts_structural_rootnix develop --command cargo clippy -p ixvm-codegen -p ix-ffi -p ix-kernel -p ixon --tests -- -D warningsnix develop --command cargo fmt --all -- --checknix develop --command lake exe ix codegen --checkgit diff --check

Both benchmark entrypoints also reject --queries 0 with a usage error before
building or proving, and a stored pre-E2 proof now reports a checked decode
error rather than aborting. The fresh post-E2 aggregate fixture verifies
against both its .ixe and .ixes inputs.

A q=1 live smoke of the converged typecheck join also executed, proved, and
verified direct shape 2 successfully: 1.221742 s execution, 26,738,665,748 FFT
cost, 52.380108 s proving, 21,565,714,432-byte peak RSS, 401,340-byte proof,
and 0.002616 s verification. The M1-f policy harness passed plan-only smoke for
both policies and emitted the expected resumable JSON schema.

Pre-convergence real-environment gates on the 124 GB development host (kept
as WP-A/WP-B baselines for the M1-f rerun):

  • A two-shard manifest from the 5,986-constant tc-parity.ixe environment
    passes coverage and structural scheduling with threshold zero
    (2 lifts + 1 binary joins (1 structural)), and its one-shard variant
    plans 1 lifts + 0 binary joins.
  • Before E2, a one-constant q=100 production lift ran end-to-end through the
    cache and scheduler, establishing WP-A's invalidation/repair baseline:
PassResultWall
Initialproved and cached root 090bea6f…ca1535~64 s
Identical rerunverified cache hit, same root1.66 s
Truncated indexclean miss, re-proof, atomic repair, same root~38 s
Post-repair rerunverified cache hit, same root1.79 s
--jobs 2 --max-ram 400195 GiB reserved, verified hit, same root1.45 s

At scale, the measured pre-E2 4-shard Init run in Measured performance
above exercised the former backend with real shard proofs, four q=100 lifts,
two lower joins, and a root join with genuine assumption discharge. The pinned
M1-f harness will repeat this fixture under both converged policies.

Soundness summary

  • Every present child is fully verified under the verifying key selected by
    its shape-constrained kind.
  • Child function indices are constrained to IxVM verify_claim or ix_aggr,
    and every recursive child must carry the same allowed digest, pinning the
    two verifying keys and two entrypoints transitively.
  • Claims and allowed data are Blake3-bound and strictly parsed with no trailing
    bytes.
  • Flat mode reopens canonical roots and proves exact union/difference;
    structural mode proves the exact root-of-roots relation and accounts for
    every unique input assumption through either inclusion or explicit carry.
  • A bogus path, dropped candidate, extra survivor, malformed choice, or
    noncanonical assumption tree rejects. Over-carrying and duplicate subject
    leaves can only weaken the output claim; the CLI requires an unconditional
    final root.
  • Coverage is checked on the unmodified manifest, so pruning cannot conceal a
    hole or overlap; only leaves with an environment-derived owned-constant
    count of zero are removed, and contraction preserves the order and shape of
    retained leaves.
  • A singleton root is a verified shape-0 wrapper, never a raw-proof shortcut;
    the circuit binds its output digest directly to the pinned IxVM child's
    CheckEnv digest.
  • Manifest-aware verification independently repeats pruning, scheduling, and
    value folding, then verifies the one derived ix_aggr outer claim. No
    claimed proof kind or shape is accepted from wrapper metadata.
  • Proving and verification build the recursion system from the same explicit
    parameter object; any parameter change necessarily changes the recursion vk,
    allowed-system identity, and cache key.
  • Cache keys bind version, recursion-vk digest, explicit FRI bytes, and exact
    outer claim, all derived before proving; hits re-hash store content, bind
    the claim, decode without panicking, and verify natively. Corrupt, missing,
    or stale entries are misses, never trusted successes or command failures.
  • A pair is admitted only after both child slots complete; immutable
    snapshots and indexed installation make arrival order irrelevant. The
    controller owns all reservations; failures drain running work before exit.
  • Free-form structural roots are serialized as opaque commitments
    (crates/kernel/src/claim.rs); canonical openings are required only by
    protocol paths that explicitly need them.

Review map

Commit-by-commit review is recommended — each commit is feature-scoped with
its own tests (map in the first comment / commit list).

AreaPrimary filesWhat to review
Legacy regression protocolIx/MultiStark/Aggregate.lean, Ix/MultiStark.leanpre-convergence 96-byte identity and semantic/audit reference surface
Production circuitIx/Aggr/Circuit.lean, Ix/Aggr.lean80-byte identity, uniform claims, shapes 0–9, heterogeneous child pinning, structural path checks
Converged host modelIx/Aggr/Host.leanfree-form structural roots, canonical survivors, path extraction
Host statement modelIx/MultiStark/Host.leanfree-form subject roots, canonical assumptions, path extraction
Manifest parsing + pruningIx/Cli/CheckCmd.leanstrict tree parsing, validate-before-prune, unary contraction, dense remap
Aggregate driverIx/Cli/AggregateCmd.leanwrap-first/direct shape policy, singleton wrap, uniform slot claims, cache v2, per-shape RAM weights, verified DAG resume/failure draining
Root verificationIx/Cli/VerifyCmd.leanone ix_aggr outer claim, value-based environment comparison, manifest-relative hybrid reconstruction
Recursion parametersIx/MultiStark.lean, Ix/Aggr.lean, Ix/Cli/{Aggregate,Verify}Cmd.leanshared config plumbing, deterministic systems, stable FRI cache encoding
Pruned FRI multiproofsIx/Aiur/{Protocol,Semantics/BytecodeFfi}.lean, Ix/Cli/{Aggregate,Verify}Cmd.lean, crates/ffi/src/aiur/protocol.rscompact persisted proof vs expanded recursive advice boundary, checked legacy-proof failure
Cache/store boundaryIx/Store.lean, Ix/Aiur/Protocol.lean, crates/ffi/src/aiur/protocol.rswipeable namespace, checked proof decoding, corrupt-data fallthrough
Shard preparationIx/IxVM/ClaimHarness.lean, Ix/Cli/{Aggregate,Check,Verify}Cmd.leanclaim/tree-only consumers, one-pass multi-root witness closure
Native advicecrates/ixvm-codegen/src/aiur_multi_stark_runner.rsstrict framing and seven-channel key layout
Converged native advicecrates/ixvm-codegen/src/aiur_ix_aggr_runner.rs, crates/ffi/src/aiur/protocol.rsstrict path framing, shared channel-6 key layout, execute/prove passthrough
Generated executorcrates/ixvm-codegen/src/aiur_multi_stark.rsgenerated artifact; verify with ix codegen --check
Converged generated executorcrates/ixvm-codegen/src/aiur_ix_aggr.rsgenerated artifact; verify with ix codegen --check
Manifest writercrates/kernel/src/shard.rsproducer-side omission, tree remap, aggregate metadata
Claim semanticscrates/kernel/src/claim.rs, crates/ixon/src/proof.rsfree-form root contract and codec regression test
TestsTests/Aggr.lean, Tests/AggrSemantics.lean, Tests/AggrActivation.lean, Tests/MultiStark.lean93 production shape/semantic/plan checks, 132-case activation audit, retained Ix-agnostic regressions
BenchmarksBenchmarks/AggregatePolicy.lean, Benchmarks/Typecheck.lean, bench registry/dashboardspinned four-shard wrap-first/direct handoff, resumable JSON, converged direct-shape-2 join metrics, historical lift-size pins

Current limits and non-goals

  • Recursion FRI parameters stay at the q=100 defaults; the q=50 + compensating
    PoW policy is an explicit follow-up decision (the measured costs above are
    therefore the conservative end).
  • Structural roots are manifest-relative. Environment-only verification
    accepts only a bundled value equal to the canonical environment root;
    hybrid roots should be verified with both --ixe and --ixes and the
    proving threshold.
  • Scheduler RAM weights are calibration-pending placeholders (validated in
    the safe direction by the measured run); concurrent proves share the rayon
    global pool, so throughput can sub-scale — process-per-slot is a
    measurement-driven follow-up.
  • Same-key cross-process cache writes degrade to a warning and recomputation,
    never a soundness issue.
  • Dummy-call activation padding is deferred until/unless a static terminal
    circuit is selected; under the SP1 terminal it is pure cost.
  • Aggregate wrappers reuse Ixon.Proof and do not encode the proof system;
    callers must pass ix verify --aggregate.
  • M1-a–f land the converged production circuit, host/native substrate, driver,
    cache, scheduler, value-based verifier, semantic union, widened activation
    audit, and policy benchmark harness. The only remaining M1 work is executing
    the wrap-first/direct comparison on the target box. The pinned M1-f revision,
    inherited M1-e preflight, and both measured policies are one checkout/session;
    its handoff is documented in
    plans/aggregate-first-m1f-large-box-runbook.md.
  • This PR does not adopt cold-circuit grouping and does not implement a
    terminal KZG/SP1 wrapper.

Follow-ups

  1. Calibrate lift/join unit costs on a known-core box, with per-node
    flat-vs-structural attribution, and measure frontier/discharge-path counts
    at Mathlib scale (plan §11.4.1).
  2. Decide the recursion q=50 + PoW policy (§9.5) now that the mechanism
    exists.
  3. Connect the root proof to the selected terminal compression path (E3:
    single-recursion-proof SP1 guest + ix compress-root).
  4. Re-evaluate cold-circuit grouping (E4); E2's measured outer-lift reduction
    was only 6.80%, so grouping may still be material.
  5. Process-per-slot proving if measured rayon contention warrants it.
  6. Run the exact M1-f commit
    b70042d6d1a029bb8e1d541da7aef5e0613d67e6 for the combined M1-e preflight
    and wrap-first/direct policy benchmark session in the large-box handoff
    runbook.

johnchandlerburnhamand others added 22 commits August 27, 2026 16:21
Add the deterministic lift/flat/structural activation matrix and keep dummy calls deferred. Split aggregate recursion commitment/FRI configuration from IxVM defaults, share it between proving and verification, and pin the future cache encoding without changing active protocol parameters.
Precompute versioned per-slot cache keys, persist lift and join wrappers in the content-addressed store, and reuse entries only after exact claim and native outer-proof verification. Add safe proof decoding, corruption recovery, and --no-cache.
Execute ready lift and join slots as a dependency DAG under explicit job and RAM admission. Add calibration-pending slot weights, failure draining, CLI controls, and serial/parallel scheduler and proof-equivalence gates.
Add an opt-in two-child benchmark that proves singleton CheckEnv shards, lifts both proofs, and measures a verified flat join. Wire join metrics through reporting and dashboards, and record the current pre-E2 lift-size baseline.
- rust-toolchain.toml channel: 1.92 → 1.98, with the matching fenix
toolchain hash in flake.nix. The pinned fenix already carries the 1.98
release manifest, so flake.lock needs no change (fenix's nixpkgs stays
pinned via its lean4-nix follows, so the Lean toolchain is untouched).
- Drop clippy::from_iter_instead_of_collect from the workspace lints:
removed in clippy 1.98 and now warns as unknown.
- Fix the warnings new clippy 1.98 lints surface across the workspace:
chunks_exact(N) → as_chunks::<N>() where the chunk size is constant,
descending sort_by → sort_by_key(Reverse(..)), iteration over map
values via .values(), map().unwrap_or() → map_or(), a checked
division, an unwrap-after-is_some restructured into if-let, and
assorted redundant-reference/pattern cleanups (mostly cargo clippy
--fix). The two byte-gadget files keep their chunks_exact warnings
until the next commit, which rewrites those regions anyway.
CI derives its Rust version from rust-toolchain.toml, so no workflow
changes are needed.
Companion to multi-stark's update-p3 branch (c72d321 → 249b740), which
carries four soundness/robustness fixes and the Plonky3 v0.6.0 bump
(pruned FRI Merkle multiproofs: ~2x faster verification, 40-70% smaller
proofs; canonical Goldilocks serde removes proof-byte malleability).
Proofs and verifying keys are not compatible with the previous pin.
Integration:
- Lookup gained max_multiplicity, a declared per-row bound on the
multiplicity's integer magnitude feeding the newly enforced logUp
height bound Σ wᵢ·hᵢ + |claims| < p. Function-circuit slots accumulate
mutually-exclusive branch selectors, so they declare 1; committed
count columns (function return slots, the memory circuit, the byte
gadget tables) declare the new COUNT_COLUMN_BUDGET (2^32 queries per
entry).
- The VK wire format carries the bound: u64 LE max_multiplicity per
lookup, between the multiplicity node id and the arg count. The
in-circuit VK deserializer (Ix/MultiStark/SystemDeserialize.lean)
parses past it; the value is bound through the vk digest but the
height bound itself is not yet enforced in-circuit (the native
verifier enforces it).
- Message fingerprints are width-bound by default upstream (the slot
width seeds the Horner fold), which is incompatible with aiur's
branch-shared lookup slots: mutually exclusive branches superpose
messages of different natural widths into one slot at the maximum
width, so a narrow call is sent zero-padded to a width its callee's
return slot never provides, and proving fails with
UnbalancedChannel (pinned as the prove_verify_mismatched_call_widths
regression). Aiur instead declares WidthBinding::ByConstruction —
the plain Horner fold, restoring zero-padding transparency — and
takes on the prefix-freeness contract that makes it sound: every
message's natural width is a function of its constant-constrained
leading prefix (channel tag plus discriminator: fun_idx fixes
2+in+out, the memory size coordinate fixes 3+size, each gadget tag
fixes its table width), so zero-extension can only equate a padded
message with its own natural form. The contract is documented at the
channel constants in lib.rs; the declaration is applied in
AiurSystem::build and mirrored in the vk_codec decoder so decoded
VKs replay the same transcript. ByConstruction is also exactly the
fold the in-circuit verifier's logup_fingerprint already computes,
so the recursive verifier needs no fingerprint change.
- The policy is Fiat-Shamir-bound as the first observe_shape word; the
in-circuit transcript replay prepends the matching limb.
- aiur_multi_stark.rs regenerated (ix codegen) for the deserializer
and transcript changes.
- P3 v0.6.0 ships FRI query openings as pruned Merkle multiproofs,
while the in-circuit verifier consumes one authentication path per
query (its per-query control flow is a far smaller circuit than the
amortized multiproof walk). Rather than porting the walk into the
DSL, the proof advice stays in the per-query transport: multi-stark's
new advice module re-encodes a natively-verified proof by running
p3's own verification with a recording compression function and
reading each query's path back out of the recorded digest map. The
advice bytes are untrusted verifier input, never digest-bound —
the transcript binds the commitments and every expanded sibling is
authenticated against them per query — so pruning vs expansion is
pure transport and the encoding choice is sound. AiurSystem gains
proof_to_advice_bytes (FFI: AiurSystem.proofToAdviceBytes); the
recursive-verifier test feeds it instead of Proof.toBytes, whose
native wire format is still round-tripped separately. The Lean-side
proof grammar and the codegen'd verifier are byte-identical to
before — no in-circuit changes.
Claim layout, the VK wire format above and aiur's public semantics are
otherwise unchanged; the policy adds no prover or verifier work over
the previous pin.
Still open, native-verifier-only: the logUp height bound is parsed
past but not yet enforced in-circuit (a wide-arithmetic check, tracked
separately).
The Lean v4.33.1 update pinned both dependencies at revisions that
predate their Rust 1.98 bumps; their heads now carry those bumps, which
this workspace needs since rust-toolchain.toml moved to 1.98. Both
revisions stay on leanprover/lean4:v4.33.1.
- Blake3.lean 1b0fbd2 → e6e908b (Rust 1.98, plus a case-insensitive
source-directory fix), updated in lakefile.lean, lake-manifest.json
and the blake3-lean flake input. The revision keeps the
`blake3_rs_shared` target the `ix_native_decide_dynlib` pin requires.
The inherited entry in Benchmarks/Compile/lake-manifest.json was still
on the pre-4.33.1 revision and now tracks the root pin.
- lean-ffi 2a9c91e → 93c7e52 (Rust 1.98). Only bignat reaches the sp1
and zisk workspaces, so their lock files move that one package.
The !benchmark recursive phase reported n/a for every fri-verifier
metric: Benchmarks/Typecheck.lean still fed Proof.toBytes — the pruned
multiproof wire format — to executeMultiStark/proveMultiStark, so the
in-circuit verifier rejected on parse and the harness (correctly) left
the recursive fields absent rather than emit a fake datum. The
in-circuit verifier consumes the per-query advice transport
(AiurSystem.proofToAdviceBytes); proofBytes stays the reported
proof-size metric.
bench-recursion-debug had the same advice-format gap plus a stale
claim recipe: it still built the public input as 32 raw digest bytes,
predating the ClaimHarness.packedDigestKey packing bench-typecheck
uses (its own out-of-circuit sanity check failed with
InvalidPowWitness — a wrong claim diverges every challenge — and the
advice re-encoder refused the proof for the same reason). Both aligned
with the typecheck flow.
Validated end-to-end at production parameters (numQueries 100,
query PoW 20, blowup 2) on Nat.add_comm: inner prove, advice
re-encoding, and the codegen'd in-circuit verifier accepting.
Keep compact proof bytes at storage and cache boundaries, expand them only for recursive lift/join advice, reject unsupported zero-query benchmarks, and decode legacy store proofs without panicking. Refresh tests, benchmark pins, and Rust 1.98 lint compatibility.
Add Ix/Aggr, a recursive aggregation system for IxVM shard proofs that
keeps Ix/MultiStark untouched and Ix-agnostic. One entrypoint, ix_aggr,
subsumes lifting and joining: a one-byte advice shape selects wrap or
binary join over any mix of IxVM and ix_aggr children, so shard proofs
enter the recursion system directly as join children and the dedicated
lift stage disappears.
Circuit (Ix/Aggr/Circuit.lean): every shape verifies its children in
full (verify + ood_verify from the shared Multi-STARK verifier modules)
against the vk its hinted kind demands. Identity is one 80-byte
digest-bound blob - blake3(ixvm vk) || verify_claim idx ||
blake3(self vk) || ix_aggr idx - carried unchanged at every node; self
children must bind the identical blob digest, pinning both vks and both
entrypoint indices transitively. Claims are a uniform 18-word
[0, aggr_idx, allowed(8), checkEnv(8)] at every depth, so proofs of
different tree levels combine freely. Wrap shapes bind the output
digest to the child CheckEnv digest directly; pair shapes open both
CheckEnv preimages, re-root the canonical subject/assumption trees, and
prove subjects = L ∪ R, assumptions = (asmL ∪ asmR) ∖ subjects with
linear sorted merges.
Toplevel (Ix/Aggr.lean): ixAggr = MultiStark.multiStarkFull + circuit,
pruned to ix_aggr, so verify_multi_stark_proof and other unrelated
entries no longer pad aggregate proofs. The host half of the wire
contracts (allowed blob, public input packing, shape codes, keyed
preimage/tree blob framing, interpreter IO assembly) lives beside the
toplevel; Ix/Aggr/Host.lean folds CheckEnvTrees statements.
Native path: ix codegen gains the ix-aggr target
(crates/ixvm-codegen/src/aiur_ix_aggr.rs, 244 fns); its runner builds
the seven-channel IO buffer natively and routes execution through the
generated code. New FFI rs_aiur_ix_aggr_execute/_prove
(executeIxAggr/proveIxAggr) pass proofs, vks, claims, and the compact
count/key/length preimage/tree blobs without per-byte boxing, plus
Proof.ofBytesChecked for store-boundary decoding.
CLI: ix aggr --ixe E --ixes M <shard-proof>... reconstructs every
nonempty shard statement from the env, matches wrappers by claim
digest, natively pre-verifies them, folds a balanced bisection (the
canonical fold makes the root claim independent of tree shape), wraps
single-shard roots so the persisted root is always an ix_aggr proof,
checks the root closes over the env canonical tree with no residual
assumptions, and persists the wrapper.
Tests (lake test -- ix-aggr, 17 cases): all five shapes accept over
real Multi-STARK stand-in child proofs from two distinct-vk systems;
codegen'd execution matches the interpreter on output and per-circuit
query counts for wrap and pair; negatives break one binding each -
lying shape hint, tampered proof, foreign identity, wrap statement
drift, dropped assumption, padded subject set, and tree advice not
reproducing its keyed root.
Replace the legacy lift/join entrypoints with the heterogeneous ix_aggr system across aggregate, verification, cache, and codegen paths. Preserve wrap-first and direct-join policies behind one proof identity and cache namespace.
Add the converged 91-check semantic suite and a deterministic 132-case activation audit covering shapes 0 through 9 twice, with no unobserved circuits.
Replace the legacy two-shard three-entrypoint benchmark with a four-shard manifest-subtree harness for production ix_aggr wrap-first and direct policies. Enforce the q=100 serialized no-cache profile, natively verify every input and recursive output, persist ordinary aggregate wrappers, and emit resumable per-slot JSON metrics.
Port bench-typecheck --join to ix_aggr direct shape 2 while preserving the stable join metric schema. Remove the final legacy slot-spec/preimage shim and pin both M1-f four-shard plans in the focused suite.
Update the multi-stark dependency and Rust toolchain for Plonky3 0.6, along with the Rust 1.98 lint migrations required to keep the workspace warning-free. Refresh the Rust-compatible Blake3.lean pin in both root and compile-package manifests.
Adapt recursive Aiur verification to Plonky3's pruned FRI multiproofs. Native proofs retain their compact serialized representation and native verification path; the FFI expands authenticated Merkle frontiers into per-query advice only when entering the existing recursive verifier circuit.
Preserve the packed claim-digest convention in the recursion diagnostic and exercise the advice boundary in the end-to-end test and benchmark paths. CPU and CUDA recursive q1 runs produce identical 823,485-byte inner proofs and 331,273-byte outer proofs.
The q50 Vector.extract_append workload retains identical CPU/CUDA proof sizes. Inner plus outer STARK proving measures 65.87s on CPU and 8.81s with CUDA on the RTX PRO 6000, a 7.48x speedup.
PR benchmark runs execute trusted workflow YAML from the default branch while loading composite actions from the PR checkout. When Bencher data and binary caches are unavailable, the workflow checks out main under base/ and asks Lake to rebuild it without first installing the Rust channel pinned by that checkout.
Teach the existing CPU provenance action to install the base checkout's validated Rust channel and profile immediately before an uncached base build. The step is a no-op when the toolchain is already available and leaves cached benchmark comparisons unchanged.
Consume the Plonky3 0.6 batch-opening layout directly in Aiur instead of expanding every pruned Merkle frontier into one authentication path per FRI query. Sample all query indices from the unchanged transcript, sort and deduplicate them with an O(q log q) merge sort, authenticate each input and commit-phase commitment once, then retain the existing per-query reduced-opening and FRI arithmetic.
Bind every frontier to transcript-derived indices, consume boundary digests in Plonky3's level/parent/child order, reject trailing frontier elements and inconsistent duplicate leaves, and assert all native opening dimensions and sibling counts. Explicitly constrain the digest-bound protocol specialization to cap height 0, binary FRI, and a constant final polynomial. Move memo_u32_less_than into IxVM Core so both substitution and multiproof sorting share its constrained rows.
Strengthen the recursive negative test to mutate a structurally valid stage-1 commitment. Regenerate both checked-in Aiur Rust executors and retain interpreter/codegen query-count parity.
On Vector.extract_append q50, recursive-verifier FFT cost falls from 204.073B to 201.166B. CPU outer proving improves from 50.09s to 45.03s and the full CPU pipeline from 90.64s to 82.90s. GPU outer proving improves from 15.85s to 13.72s and the full GPU pipeline from 28.86s to 26.69s. The outer proof grows from 3.92 MB to 4.17 MB.
Validated with the MultiStark primitive suite, recursive honest/tamper/parity tests, codegen --check, release workspace clippy, release CUDA clippy, rustfmt, and diff checks.
@johnchandlerburnham

Copy link
Copy Markdown
MemberAuthor

Init full proving report

Revision: 82dcc9dba36d0cfd6c753c6878ab649643a52026
Host: 64 vCPU, 495 GiB RAM, 256 GiB swap
Final result: one verified ix_aggr FRI proof with no remaining assumptions.

Compilation and sharding

StepResult
Compile sourceBenchmarks/Compile/CompileInit.lean
Ix compiler time1.876s
Constants compiled65,994
init.ixe size194,296,853 bytes (185.3 MiB)
Static sharding time3.1s
Manifest16 nonempty shards
Retained/owned constants56,621
Manifest size2,619,404 bytes (2.50 MiB)
Predicted FFT per shard4.160e11–4.197e11
Predicted FFT spread1.01×

The 1.876-second compile measurement is the Ix compiler itself; it excludes the initial one-time dependency download and Lake build.

Stage 1: base shard proofs

All 16 shard proofs were persisted and individually verified against their corresponding manifest claims.

MetricSerial baselineTwo-way run used for aggregation
Concurrency12
End-to-end wall time24m 16.76s15m 53.93s
Sum of shard prover times22m 43.23s30m 08.14s
Per-shard time range74.08–113.97s98.94–142.29s
Median shard time82.66s108.75s
Highest individual RSS219.8 GiB219.8 GiB
Combined process-tree peak358.2 GiB
Proof count1616
Total proof-wrapper size369,995,038 bytes
Mean proof-wrapper size23,124,690 bytes
Proof-wrapper range22,714,520–23,998,692 bytes

Two-way concurrency gave a 1.53× wall-time speedup despite increasing total prover-seconds by about 33% from CPU/memory contention.

Stage 2: recursive aggregation

Policy:

  • Wrap-first
  • --structural-above 4096
  • --jobs 2
  • --max-ram 450
  • Default q=100 recursion parameters
  • Aggregate cache enabled

Plan:

OperationCount
Shard wraps16
Binary joins15
Structural joins14
Flat joins1
Total recursive slots31

Results:

MetricResult
Wall time45m 06.61s
Peak resident memory379.9 GiB
Average CPU utilization4,192% (~42 cores)
Cache hits0
Freshly proved/cached slots31
Swap used0 bytes
Exit status0
Final proof-wrapper size9,596,044 bytes (9.15 MiB)

Final proof

Root address:

80f6844f968920abaef8d8c750a1a1dc1ebe32657a3cfb824e1810bcd65c8814

Final claim:

CheckEnv(6dc8f821a989c6f7b9d520b053ef8edbd582d2408d7b6a6ff72b908666e03b6f, none)

Both validations passed:

  • Cryptographic-only aggregate verification
  • Manifest-aware verification against init.ixe, init-16.ixes, and structural threshold 4096

End-to-end measured time

Using the two-way Stage 1 run:

ComponentTime
Ix compilation1.876s
Sharding3.1s
Stage 1 proofs15m 53.932s
Stage 2 aggregation45m 06.61s
Measured core pipeline1h 01m 05.52s

This total excludes the one-time dependency build/download and a few seconds of manifest and final-root verification overhead. Using serial Stage 1 instead would make the measured core pipeline approximately 1h 09m 28.35s.

Use the PR77 lean-ffi and multi-stark pins, remove the stale width-binding and multiplicity-bound protocol fields, and regenerate the recursive and aggregate verifier sources.
Expose an opt-in x86_64-linux cuda shell with NVCC 13.2, sm_120 code generation, and safe host libcuda discovery while leaving the default development shell unchanged.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@johnchandlerburnham@samuelburnham@arthurpaulino
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root - #598

Open
johnchandlerburnham wants to merge 24 commits into
mainfrom
jcb/aggregate-first
Open

Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root#598
johnchandlerburnham wants to merge 24 commits into
mainfrom
jcb/aggregate-first

Conversation

@johnchandlerburnham

@johnchandlerburnhamjohnchandlerburnham commented Aug 27, 2026

Copy link
Copy Markdown
Member

Summary

This PR implements the aggregate-first path for Aiur Multi-STARK proofs. The
production path now runs through one ix_aggr recursion entrypoint: it wraps
independently-proven IxVM shards, folds their CheckEnv statements along the
manifest's bisection tree, and persists one recursive root proof with a
uniform 18-word outer claim. Every completed recursive slot is persisted in a
verified, content-addressed version-2 resume cache, and ready slots are proven
in parallel under explicit job and RAM limits.

The entrypoint has two pair-fold modes:

  • Flat shapes perform a canonical set union/difference and are efficient for
    small lower nodes.
  • Structural shapes commit to nodeHash(leftRoot, rightRoot) in one hash and
    discharge assumptions with Merkle inclusion paths. Their 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 (default
4096 subject leaves): flat below the threshold, structural above it.

IxVM shard proofs
│
├── ix_aggr shape 0 (default wrap-first policy)
│ or remain raw with --direct-joins
▼
ix_aggr proofs carrying uniform CheckEnv statements
│
├── flat ix_aggr shapes 2–5 below the configured subject threshold
│ subjects := canonical(subjectsL ∪ subjectsR)
│ assumptions := (assumptionsL ∪ assumptionsR) ∖ subjects
│
└── structural ix_aggr shapes 6–9 above the threshold
subjects := nodeHash(subjectRootL, subjectRootR)
assumptions := candidates carried or discharged by inclusion path
▼
Ixon.Proof {
claim := CheckEnv(manifest-relative subject root, none),
proof := one recursive Multi-STARK root proof
}

A manifest with one retained (nonempty) shard is also a valid deliverable: its
root is one shape-0 wrapper, with no self-join padding (including under
--direct-joins). Zero-constant manifest leaves are pruned after raw coverage
validation:

raw manifest
|
| validate exact environment coverage before pruning
v
drop zero-constant leaves + contract unary tree nodes + remap retained ids
|
+-- one retained leaf --> wrap once and persist the ix_aggr root
|
`-- multiple leaves --> fold flat/structural joins as above

This is the stage-2 aggregation layer from
plans/aggregate-first-pipeline.md: shard proofs are aggregated before any
terminal KZG/SP1 compression, so a later wrapper only needs to consume one root
proof. Terminal compression remains outside this PR.

Section 14 implementation is complete through M1-f in this branch. The
ap/ix-aggr single-entrypoint base is reconciled with WP-E2's compact stored
proof / expanded recursive-advice boundary, and its one ix_aggr entrypoint
supports wraps, flat pairs, and structural pairs across both IxVM and recursive
children. ix aggregate now uses that backend for planning, proving, cache
resume, and final verification; ix verify --aggregate derives one uniform
outer claim and performs value-based environment/manifest checks. The old
ix aggr command and all three-entrypoint production wiring are retired. The
older Multi-STARK aggregate implementation remains as an Ix-agnostic regression
surface, not as a CLI backend. Production semantics, activation coverage, and
benchmarking now live entirely against ix_aggr. M1-f lands the pinned
four-shard bench-aggregate-policy handoff and ports the stable typecheck join
metric to direct shape 2; only execution of the policy comparison on the large
box remains. Operationally this is one future handoff: Arthur will check out
b70042d6d1a029bb8e1d541da7aef5e0613d67e6 once, run the inherited M1-e
correctness suite as untimed preflight, and then benchmark both policies. There
is no separate M1-e box run.

Measured performance (2026-08-29)

Arthur Paulino ran the pre-convergence pipeline end-to-end on four real Init
shard proofs (shards 8–11 of a 16-way partition, 12,493 constants; base shard
proving excluded; native verification of each output included; ~512 GB box).
These are the standing M1-f comparison baseline; the converged wrap-first and
direct policies have not yet been rerun on that box. The recursion used the
conservative q=100 / PoW 20 defaults this PR keeps:

StageTimePeak RAMOutput proof
Lift × 498.8–103.0 s (mean ≈ 101 s)186.8–195.8 GiB8,162,462 B each
Join 8+9, 10+1148.9 / 48.5 s~102.5 GiB9,502,843 B
Root join85.8 s156.9 GiB9,455,498 B
Total (serialized)10:23 wall195.8 GiB9,455,498 B

Notes: lift cost is shard-content-independent (four lifts within ±2%, tracking
the ~constant 20–23 MB shard-proof shape); the measured lift peak brackets the
scheduler's 195 GiB weight placeholder exactly; and an unserialized run of all
four lifts OOM'd at ~492 GiB — precisely the behavior the WP-B admission gate
prevents (on a 512 GB box its weights admit two lifts). Because shards 8–11
are a strict partition subset, the root carries a real frontier assumption
set, so the joins exercised genuine discharge work. Full analysis, including
the comparison against direct (unlifted) IxVM joins, is in
plans/aggregate-first-pipeline.md §3.4 and the Zulip thread.

What lands

Protocol / circuit layer:

  • One production recursion entrypoint, ix_aggr, under one verifying key.
    Shapes 0–1 wrap one IxVM/recursive child, shapes 2–5 fold flat pairs, and
    shapes 6–9 fold structural pairs for every child-kind combination.
  • Recursive validation of wrap, flat-pair, and structural-pair children with
    transitive verifying-key, function-index, and allowed-system binding. A
    shape hint selects the verified form but cannot weaken its checks.
  • Strict in-circuit CheckEnv decoding and canonical flat set folding.
  • Structural subject folding with per-candidate Merkle discharge paths while
    keeping output assumption sets canonical and deduplicated.
  • One 80-byte protocol identity containing the IxVM/self verifying-key
    digests and their two accepted entrypoint indices; every recursive node has
    the same 18-word outer-claim layout regardless of shape.
  • Structural outputs bind nodeHash(leftRoot, rightRoot) and account for each
    unique assumption candidate through a strict Merkle-path-or-carry choice.
  • Host helpers construct structural roots, survivors, and path advice. Channel
    6 carries both the one-byte shape at key [0] and candidate path payloads at
    raw 32-byte address keys; the key shapes are disjoint.
  • Lean/Rust execute and prove FFI accept a compact keyed path blob, whose
    framing is decoded strictly before the circuit independently validates
    payload semantics.
  • The generated ix_aggr executor is regenerated at 2,061,361 bytes / 248
    Aiur functions and parity-tested against the interpreter.

Host driver and operations:

  • Strict .ixes aggregation-tree parsing and post-order lowering, with a
    balanced fallback for legacy manifests; validate-then-prune removal of
    zero-constant leaves with unary tree contraction (and a Rust writer that
    stops emitting them).
  • Single-shard roots: a one-retained-shard manifest always produces one
    shape-0 ix_aggr wrapper, including under --direct-joins.
  • A monotone leaf-count scheduler controlled by --structural-above, with a
    default wrap-first plan (shapes 0 then 5/9) and an explicit
    --direct-joins plan that derives heterogeneous shapes from actual child
    kinds.
  • Versioned, content-addressed aggregate cache and resume (WP-A): version 2
    binds the uniform outer claim, and reuse occurs only after content-digest,
    claim, decode, and native verification all pass; --no-cache bypasses.
  • Parallel execution of ready wraps/pairs as a dependency DAG under --jobs
    and --max-ram (default 92% of MemTotal), heaviest-first, with failure
    draining (WP-B). Admission weights are shape-aware: 4 GiB for a retained
    raw IxVM leaf, 195 GiB for wraps/self pairs, 390 GiB for IxVM/IxVM pairs,
    and 340 GiB for mixed pairs.
  • A recursion-parameter split (WP-E1): aggregate proving/verification share an
    explicit RecursionParameters value, defaulting to today's q=100 values;
    the q=50+PoW policy decision is deliberately deferred.
  • A deterministic 132-case production activation audit across shapes 0–9:
    two controlled child trace heights, both wraps, every flat/structural
    child-kind pair, optional assumptions per side, and discharge/carry. The
    complete matrix runs twice with a stable signature and observes every
    catalogued circuit; dummy-call padding remains intentionally deferred.
  • Claim-only shard preparation (no discarded dependency-byte closure walks)
    with a one-pass multi-root witness closure for proving.
  • ix aggregate --ixe E --ixes M [--structural-above N] [--direct-joins] [--jobs N] [--max-ram G] [--no-cache] [--plan-only] <proof>... and
    single-entrypoint aggregate verification with cryptographic-only,
    environment-bound, and manifest-bound value checks. The duplicate
    ix aggr command is removed.
  • Opt-in aggregate benchmarks: a tiny two-shard Init pair end-to-end bench and
    a flat-join bench (singleton CheckEnv shards → two lifts → verified flat
    join), with join metrics wired into reporting/dashboards, the pre-E2 lift
    proof size pinned at 7,986,166 bytes, and the post-E2 delta measured.
  • Positive, transitive-recursion, scheduler, cache, pruning, parity,
    malformed-input, and semantic-tampering tests: the aggregate-first suite
    has 58 cases, plus the separate activation matrix.

Protocol design

One recursion system, one entrypoint

Production builds Aggr.ixAggr, the shared Multi-STARK verifier closure pruned
to one ix_aggr entrypoint. A one-byte advice hint selects the exact verified
shape:

ShapeChildrenFold
0IxVMpass-through wrap
1ix_aggrpass-through wrap
2–5IxVM/ix_aggr pair (2 + 2·left + right)canonical flat fold
6–9IxVM/ix_aggr pair (6 + 2·left + right)structural fold

The hint is advice, not authority: each arm verifies the child proof(s) under
the key required by the encoded child kinds, then requires that kind's exact
claim layout. A wrong shape therefore fails proof verification, function-index
binding, or claim decoding.

Uniform public statement and pinned protocol identity

Every wrap and pair exposes the same public-input shape:

ix_aggr(allowed_digest: [G; 8], out_claim_digest: [G; 8])

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:

  1. the 80-byte allowed-system blob; and
  2. the serialized output CheckEnv claim.

The allowed blob is:

blake3(ixvm_vk)
|| verify_claim_idx:u64le
|| blake3(ix_aggr_vk)
|| ix_aggr_idx:u64le

The circuit requires exactly 80 bytes. 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
another function in those systems; binding the two accepted 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.
Every persisted aggregate outer claim is consequently the same 18-word form:
the ix_aggr function selector followed by the packed identity and output
claim digests. Shapes are witness choices and do not become root-kind metadata.

Child-proof validation and transitive pinning

Each pair reads the verifying key required by each child kind, checks its
Blake3 digest against the allowed blob, and deserializes it strictly. Wraps do
the same for their one child. For every child the selected arm:

  1. reads and fully consumes one proof and exactly one outer claim;
  2. runs the Multi-STARK structural verifier;
  3. runs ood_verify, including Fiat-Shamir replay, lookup accumulators,
    quotient/OOD checks, Merkle openings, and FRI verification; and
  4. decodes the statement according to the constrained child kind and function
    index.

The accepted forms are:

  • IxVM child: its system digest must equal blake3(ixvm_vk). Its claims
    digest opens to exactly one 10-word IxVM claim at the pinned
    verify_claim_idx, whose digest opens to a strict serialized CheckEnv
    claim.
  • ix_aggr child: its 18-word claim must use the pinned ix_aggr_idx; its
    first public digest must equal the current allowed_digest, transitively
    pinning both systems and both entrypoints; and its second digest opens to its
    output CheckEnv claim.

Any other index, shape, digest, trailing byte, or failed proof check rejects.
This keeps wrap→pair, flat→pair, and structural→structural composition on one
statement format.

Flat canonical folding

For child statements (S_L, A_L) and (S_R, A_R), flat shapes 2–5 enforce:

S_out = S_L ∪ S_R
A_out = (A_L ∪ A_R) ∖ S_out

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 u32 words using the
full-domain u32_less_than primitive. The implementation never uses Aiur
pointer 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

Structural shapes 6–9 perform the same allowed-blob binding, child-key
binding, two child-proof verifications, child decoding, and output-claim
binding as the flat shapes. They replace all subject-tree loading and set
re-rooting with:

S_out.root = nodeHash(S_L.root, S_R.root)

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:

  • carried (0): no bytes may follow, and the candidate must be the next
    value in the output assumption list; or
  • discharged (1): a bounded Merkle path from
    leafHash(candidate) to S_out.root must verify.

Path payloads are strict:

0
1 || count:u8 || repeated(side:u8 || sibling[32])

count is limited to 64. side = 0 hashes nodeHash(sibling, current) and
side = 1 hashes nodeHash(current, sibling); any other side rejects. The
payload 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

All ix_aggr shapes use the same seven-channel IO layout. Digest/root bindings
are checked before the corresponding bytes are decoded.

ChannelKeyValue
0[0], [1]first and optional second child proof advice bytes
1[kind]verifying-key bytes (0 = IxVM, 1 = ix_aggr)
2[0], [1], [2]left/right outer claims and output CheckEnv claim
3[0]80-byte allowed-system blob
4packed Blake3 digestnested claim preimages
5raw 32-byte addressserialized canonical trees keyed by root
6[0] / raw candidate addressshape byte / carried-or-discharged path choice

Lean passes channels 4–6 across FFI as compact framed blobs:

count:u32le || repeated(key[32] || payload_len:u32le || payload)

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.

Wraps receive no tree or path advice. Flat pairs receive full
subject/assumption tree advice and an empty path blob. Structural pairs receive
only the input/output assumption trees plus one path choice for every
deduplicated candidate; subject trees are never opened.

Manifest and host pipeline

.ixes aggregation tree

parseIxesManifest exposes both shard block lists and the optional binary
aggregation tree stored at the manifest tail. It:

  • requires nonzero, contiguous shard IDs;
  • validates option and tree tags;
  • requires tree leaves to be exactly the manifest shard set;
  • rejects duplicate/out-of-range leaves and trailing bytes; and
  • synthesizes a balanced ascending-ID tree for legacy manifests without a
    tree tail or with an explicit absent-tree tag.

The tree lowers to post-order FoldOp slots. Every join refers only to earlier
slots and the last slot is the root, providing a direct serial schedule and the
basis for cache keys and parallel execution.

Validate first, then prune empty manifest leaves

IxesManifestView retains the original shard id for every dense shard slot.
pruneEmpty:

  1. counts the environment constants owned by every raw shard;
  2. drops only shards with a count of zero;
  3. contracts aggregation-tree nodes with a removed child;
  4. densely remaps retained leaf indices; and
  5. returns the retained per-shard subject counts with the pruned view.

The aggregate and manifest-aware verifier paths deliberately call
shardsCover on the raw manifest before this transformation. Coverage
still establishes that every environment constant is owned exactly once. The
subsequent zero count is therefore evidence that a removed leaf contributes no
subject; pruning is not allowed to hide a missing or duplicated constant.

Original shard ids are preserved separately from dense scheduling ids, so CLI
plans and proof-binding errors continue to identify the source manifest shard.
An all-empty manifest is rejected because it cannot produce a root.

For a tree node node(left, right), pruning follows:

both retained -> node(left', right')
only left retained -> left'
only right retained -> right'
neither retained -> removed

The Rust .ixes writer (crates/kernel/src/shard.rs) also normalizes new
manifests before serialization — block-empty shard records are omitted, ids
rewritten densely, the tree pruned/contracted/remapped, and cross-ingress
recomputed. The Lean-side pass remains necessary for legacy manifests and for
the stronger environment-relative condition (a shard may contain blocks yet
own zero constants).

Monotone structural scheduling

The host counts actual owned constants for each shard after the disjoint-cover
check. schedulePlan annotates every slot with its cumulative subject count and
chooses a structural pair exactly when:

left.subjectCount + right.subjectCount > structuralAbove

The default threshold is 4096; --structural-above 0 makes every join
structural 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.

By default, every leaf is an ix_aggr shape-0 wrap, so binary slots are shape
5 below the threshold and shape 9 above it. With --direct-joins, a
multi-shard plan keeps leaves as raw IxVM children and derives each pair shape
from its actual child kinds: 2–5 for flat pairs or 6–9 for structural pairs.
The monotone threshold rule is unchanged.

The proving loop checks reconstructed subject counts against the schedule
before starting expensive work.

Singleton roots

ix aggregate imposes no two-shard minimum. The fold plan for one retained
leaf contains one shape-0 wrap and zero pairs; that wrapper and its CheckEnv
claim are persisted as the aggregate root. Even --direct-joins wraps a
singleton because the deliverable must verify under the one production
entrypoint. A self-pair would add work and manufacture a different statement.
Consequences:

  • exactly one proof is required per nonempty shard;
  • --plan-only reports one wrap and zero binary pairs for a singleton;
  • multi-shard flat/structural scheduling is unchanged; and
  • the final unconditional-root check always applies to an ix_aggr root.

ix aggregate

ix aggregate --ixe ENV.ixe --ixes SHARDS.ixes \ [--structural-above N] [--direct-joins] [--jobs N] [--max-ram G] [--no-cache] \ SHARD_PROOF_ADDR...

The command:

  1. deserializes the environment and manifest and verifies a disjoint exact
    shard cover on the raw manifest, then prunes zero-constant leaves;
  2. reconstructs every retained shard's exact CheckEnv claim and canonical
    trees (claim-only preparation — no dependency-byte closure);
  3. matches proof arguments to shards in any order by bundled claim digest,
    then requires exact claim equality and one proof per nonempty shard;
  4. natively verifies every IxVM shard proof before expensive recursion work;
  5. builds the IxVM and ix_aggr recursion systems and the pinned 80-byte
    allowed blob;
  6. derives every slot's statement, child kind, shape, uniform outer claim, and
    version-2 cache key up front;
  7. executes ready wraps and flat/structural pairs as a RAM-gated dependency
    DAG, resuming verified cached slots and persisting each completed wrapper
    (raw direct-policy leaves are verified inputs, not cache slots);
  8. canonicalizes all leaves under the final subject tree and checks that they
    reproduce the environment's canonical root;
  9. requires no remaining assumptions and natively verifies the outer proof at
    the one ix_aggr root entrypoint; and
  10. persists an Ixon.Proof containing the output CheckEnv claim and proof.

--plan-only stops after coverage validation and scheduling, resolving and
printing the scheduler policy without loading proofs or compiling recursion
systems:

ix aggregate --ixe ENV.ixe --ixes SHARDS.ixes \ --structural-above 0 --plan-only

ix verify --aggregate

Aggregate roots reuse Ixon.Proof, so verification is selected explicitly.
The verifier never accepts root-kind metadata. It serializes the bundled
CheckEnv value and reconstructs the only valid outer claim:

claim_bytes := serialize(CheckEnv(...))
outer_claim := ix_aggr(
blake3(allowed_blob),
blake3(claim_bytes))
InvocationCryptographic entrypointExpected statement value
ix verify --aggregate <proof>ix_aggrbundled claim; cryptographic proof only
ix verify --aggregate --ixe E <proof>ix_aggrcanonical environment root, unconditional
ix verify --aggregate --ixe E --ixes M <proof>ix_aggrpruned manifest fold, unconditional

Environment-only mode compares the bundled value to the canonical environment
root; a manifest-relative structural value therefore rejects naturally, with
no root-kind branch. Manifest-aware verification repeats pruning and the fold
schedule (the threshold must match the proving run), reconstructs every leaf
statement, folds the exact hybrid value, requires no assumptions, and verifies
the same ix_aggr outer claim. Existing non-aggregate proof and
shard-composition verification paths are unchanged.

Claim-only shard preparation

shardCheckEnvClaimTrees constructs only the CheckEnv claim and its
canonical subject/assumption trees. Aggregate planning, shard-digest
reconstruction, and aggregate verification use this path because none of them
consumes dependency bytes. shardCheckEnvClaim remains the witness-building
entry point; it reuses the same claim/tree result and computes the byte
closure once from the union of the owned and available primitive roots.
Reachability distributes over that root union, so this preserves the old
closure while avoiding repeated traversal of shared subgraphs.

Recursion parameters (WP-E1)

Aggregation and aggregate verification consume one shared
MultiStark.RecursionParameters value containing the recursion commitment and
FRI configurations, carried through runAggregateCmdWith/runVerifyCmdWith
into the shared MultiStark.buildRecursionSystem. The CLI entrypoints pass
defaultRecursionParameters, whose fields are exactly today's canonical Aiur
defaults. This is a mechanism split, not a policy change:

  • ordinary IxVM proving and verification remain on the canonical q=100
    configuration;
  • recursion proofs also remain q=100 with the same commitment and PoW values;
  • no new CLI flag selects security parameters; and
  • choosing q=50 plus a compensating PoW policy remains the explicit §9.5
    follow-up decision.

The recursion verifying key serializes and binds both parameter sets, and its
digest is included in every aggregate node's allowed-system blob, so any parameter
change changes the recursive protocol identity without a public-input or
allowed-blob format change. RecursionParameters.cacheFriBytes pins the
40-byte fri_params_ser cache-key component:

logFinalPolyLen:u64-le || maxLogArity:u64-le || numQueries:u64-le ||
commitProofOfWorkBits:u64-le || queryProofOfWorkBits:u64-le

Commitment parameters need no second cache component: changing them changes
the recursion-vk digest already present in the key.

Pruned FRI multiproofs (WP-E2)

The four-commit origin/update-multi-stark series is transplanted onto this
stack: Rust 1.98, the 249b7405 multi-stark revision on P3 v0.6, the matching
Blake3/Lean-FFI pins, and recursive proof-advice encoding. The new proof wire
format deduplicates shared Merkle siblings across sampled FRI queries. Width
and trace-height binding and the vk's checked max_multiplicity travel with
the dependency update; no aggregate claim or public-input format changes.

There are now deliberately two proof encodings. Proof.toBytes is the compact
wire form used by wrappers, native verification, the store, and WP-A cache
entries. Recursive execution still expects one path per query, so callers use
AiurSystem.proofToAdviceBytes immediately before a wrap or pair. The CLI,
pair/typecheck benchmarks, semantic tests, and activation audit all enforce
that boundary. Cached proofs stay compact at rest and are expanded only when
consumed as a parent pair's child.

The dependency's compact format is not backward-decodable. Store-facing CLI
verification therefore uses Proof.ofBytesChecked and reports a normal error
for legacy or corrupt proofs instead of crossing the unchecked decoder and
aborting. P3 v0.6 also requires a positive FRI query count; benchmark CLIs
reject --queries 0, and the old q=0 join smoke gate is replaced by q=1.
The Rust 1.98 migration also applies its new strict-Clippy equivalents for
fixed-size digest chunking and fallible metadata-size lookup; these are
mechanical, semantics-preserving rewrites.

Verified cache and resume (WP-A)

Before the first proof, the driver derives every fold slot's CheckEnv
statement, exact outer Aiur claim, and cache key, using the same
flat/structural fold policy as proving — so a manifest rebudget or statement
change invalidates only the changed subtree. The key:

blake3(
aggregateCacheVersion:u64-le ||
blake3(recursion_vk) ||
recursion_fri_params:40-bytes ||
serializeClaims([outer_claim])
)

The uniform outer claim already includes the ix_aggr entrypoint index and
public input, hence the allowed-system blob, output CheckEnv, both
verifying-key identities, and both accepted entrypoint indices. Each completed
recursive slot is stored as an Ixon.Proof wrapper in the content-addressed
~/.ix/store; a raw --direct-joins leaf is only an input and is not cached.
The wipeable index ~/.ix/cache/aggregate/<key> holds only the wrapper's store
address and is updated via temp-file + atomic rename. M1-d bumps
aggregateCacheVersion from 1 to 2, so old outer-claim entries miss cleanly.

A hit is an untrusted hint, not authority. Reuse requires all of: a
well-formed index address; store bytes whose Blake3 digest equals it; a
decodable wrapper; a bundled claim exactly equal to the precomputed slot
CheckEnv; a safely decoded Aiur proof (Aiur.Proof.ofBytesChecked — the
former trusted-byte constructor panicked on truncated bincode, and malformed
cache bytes must never abort the command); and native verification under the
exact precomputed outer claim and local recursion system. Any failure logs a
miss and re-proves. Cache write failures are warnings. On a valid hit the
host reconstructs the slot's preimages from the manifest statement, so
parents need no trusted metadata from the cache. --no-cache bypasses index
reads and intermediate writes while still persisting the final root wrapper.

RAM-gated parallel scheduling (WP-B)

The post-order fold executes as a dependency DAG: leaves are ready
immediately; a join becomes ready when both child slots have completed. The
controller sorts ready work by descending RAM weight (slot number breaks
ties) and starts one dedicated IO.asTask per admitted slot around the
existing proof FFI. Two independent limits govern admission:

  • --jobs N caps active slots; absent or 0 means every ready slot may run
    if it fits the budget;
  • --max-ram G caps total reserved GiB in flight, defaulting to 92% of Linux
    MemTotal.

As in the existing Rust RamGate, a slot whose estimate exceeds the entire
budget is admitted only while nothing else runs — a conservative estimate
serializes work rather than deadlocking. The calibration-pending per-shape
weights are:

raw IxVM leaf = 4 GiB
shape 0/1 wrap = 195 GiB
shape 2/6 IxVM + IxVM = 390 GiB
shape 3/4/7/8 mixed pair = 340 GiB
shape 5 self + self flat = 195 GiB + 1 MiB × subject leaves
shape 9 self + self struct = 195 GiB

The 195 GiB wrap reserve carries forward the recorded q=100 lift upper bound —
the measured peaks above (186.8–195.8 GiB) bracket it exactly. The 390/340 GiB
direct and mixed estimates deliberately keep the non-default policy out of the
256 GiB fleet tier pending M1-f calibration; measured self-pair peaks
(102.5–156.9 GiB) make their weights conservative in the safe direction. Real
calibration on a known-core box will tighten them without changing scheduler
semantics.

Results are installed by slot index, never arrival order; each worker gets an
immutable snapshot of its completed children, so parallel timing cannot
change parent advice. On failure the controller stops admitting, drains every
running proof, and reports the lowest failed slot; successful independent
tasks may finish publishing valid cache entries. Concurrent proofs share
Aiur's rayon global pool, so throughput can sub-scale even when memory
permits multiple slots; process-per-slot execution is a measurement-driven
follow-up.

Converged activation audit (WP-D + M1-e)

lake exe IxTests aggregate-activation is a dedicated diagnostic kept out of
the default suite. It executes the generated production ix_aggr path across
4 wraps and 128 pairs: two controlled child trace heights, shapes 0–9,
optional assumptions independently per side, complete discharge versus
explicit carry, and all four child-kind combinations in both flat and
structural modes. Every execution returns per-circuit (uniqueRows, totalHits); the runner performs the full 132-case matrix twice and rejects any
difference before rendering a deterministic per-shape Markdown report.
Current signature:

263a3087d42340613b012d50a9416e23549f85aa1ea425e86bf0a216c7b3e27d

Result: 183 audited circuits — 154 active in every case, 29 input-dependent,
zero never observed. The variable set concentrates in the expected paths:
wrap dispatch, canonical set folding, structural Merkle discharge, and
child-kind decoding. Fixed-height Bytes1/Bytes2 circuits are
outside the execute FFI's query-count array and are called out explicitly.
The audit records information only; dummy calls would add cost for the
currently planned SP1 terminal and should be chosen only if a future static
terminal requires input-independent activation.

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.executeMultiStarkJoin for generated or interpreted flat
    and structural join execution;
  • AiurSystem.proveMultiStarkJoin for witness generation and proving;
  • JoinAdvice, JoinPreimage, JoinTree, JoinPath, strict framed decoders,
    and the seven-channel join_io_buffer;
  • the matching rs_aiur_multi_stark_join_execute and
    rs_aiur_multi_stark_join_prove externs; and
  • Aiur.Proof.ofBytesChecked for panic-free store-boundary proof decoding.

The retained three-entrypoint executor is parity-checked against the bytecode
interpreter on both output and every circuit's unique-row/total-hit counts. The
combined verifier contains 247 Aiur functions; the regenerated Rust file is
2,239,894 bytes (ixvm: 5,807,375 bytes / 780 functions). Most line churn is
generated code. The protocol source of truth is
Ix/MultiStark/Aggregate.lean; the native advice contract lives in
aiur_multi_stark_runner.rs. It is now an Ix-agnostic regression/reference backend,
not the production CLI backend.

The production ix_aggr executor is independently parity-checked for wrap,
flat, and structural shapes. It contains 248 Aiur functions and its generated
Rust file is 2,061,361 bytes. Its source of truth is
Ix/Aggr/Circuit.lean, with native advice construction in
aiur_ix_aggr_runner.rs; all three generated artifacts are checked with
ix codegen --check.

Benchmarks

M1-f replaces the legacy two-shard/three-entrypoint runner with
bench-aggregate-policy, a production ix_aggr benchmark for one exact
four-shard fixture. It selects the four retained leaves from a validated and
pruned manifest, contracts the surrounding tree without changing orientation,
and independently reconstructs the expected host root. Wrap-first uses shape 0
leaves; direct mode keeps IxVM leaves raw. Both policies then select their flat
or structural heterogeneous shapes from the production plan. Measured runs
require q=100, jobs=1, --no-cache, and JSON output; the harness natively
verifies all four inputs and every recursive output, persists every completed
slot as an ordinary Ixon.Proof, and writes resumable per-slot and whole-run
timing/RSS/proof metadata.

The existing bench-typecheck --recursive --join row keeps its stable
join-* reporting/dashboard schema, but its join phase now exercises direct
ix_aggr shape 2 instead of the retired join_two circuit. Historical size
baselines remain recorded: 7,986,166 bytes (7,986,204 with the Ixon.Proof
wrapper) for a one-constant q=100 lift under the pre-E2 247-function system,
versus 7,443,023 bytes (7,443,061-byte wrapper) after WP-E2, a 543,143-byte /
6.80% reduction. Pruned multiproof length depends on sampled query-path
overlap, so a positive-PoW output is now a sample rather than an exact
byte-length pin. The deterministic q=100/PoW-0 regression signal is 7,447,279
bytes and 38,850,942,825 recursive-verifier FFT cost. The target-box policy
comparison is the remaining measurement, not remaining harness work.

Test coverage

The legacy aggregate-first suite uses a small stand-in child system so it can
create real Multi-STARK child proofs without the tens to hundreds of GiB
required by a full recursion proof. Those proofs are consumed by the retained
join circuits, so proof validation, statement decoding, set/path folding,
native advice, and transitive recursion are exercised end-to-end at execution
time.

The 57 passing legacy aggregate-first cases cover:

  • canonical flat and structural host folds; manifest parsing, validation, and
    post-order lowering;
  • distinct lift/flat/structural compiled entrypoint identities;
  • honest flat union and cross-child discharge; structural path discharge plus
    a carried assumption;
  • flat-join and structural-join outer claim layout and native verification;
  • transitively pinned flat and structural children; rejection of a child
    carrying a different allowed digest;
  • generated/interpreter parity for both join modes; threshold scheduling that
    is flat below and structural above monotonically;
  • strict keyed-blob framing; wrong-root and tampered-sibling paths; a missing
    path choice; a carried candidate omitted from the output; the obsolete
    88-byte allowed blob; a flat join fed a genuinely free-form structural
    child root; omitted/extra assumptions or subjects; noncanonical unsorted
    trees; tampered child proofs;
  • a synthetic three-leaf manifest with empty outer leaves: raw coverage before
    pruning, contraction, dense remapping, and singleton-lift classification;
  • claim-only shard preparation preserving exact subject/assumption trees,
    with the one-pass witness closure matching the legacy per-owned-root union;
  • exact legacy verifier reconstruction of a lift outer claim and native
    verification under it;
  • byte-for-byte equality of the shared default recursion builder with the
    former direct construction; the exact 40-byte FRI cache encoding;
    independent recursion-vk identity changes for FRI and commitment overrides;
  • cache-key invalidation across version, recursion vk, FRI parameters, and
    outer claim; whole-plan statement/claim/key derivation before proving;
    atomic index creation, corrupt-index detection and repair; a
    content-addressed, claim-bound, natively verified resume hit; mismatched
    claim rejection; corrupt store-content fallback; checked rejection of
    malformed proof bytes without a Rust panic;
  • heaviest-first admission with deterministic tie-breaking; job and byte
    ceilings; dependency release; admit-alone oversize handling; the flat-join
    affine weight and MemTotal parser; jobs=1/jobs=2 payload equality;
    failure draining without admitting a dependent join; jobs=1/jobs=2
    byte-identical recursive wrappers under canonical zero-PoW grinding, with
    each scheduled child decoded and verified; and
  • Rust writer normalization plus round-tripping a genuinely noncanonical
    structural CheckEnv root.

The wrapper-byte scheduler gate uses zero PoW deliberately: with the pinned
positive-PoW implementation, rayon's find_any selects any passing grind
witness, so two valid serial proofs already differ in bytes. Production
jobs=1/jobs=2 correctness is the stronger protocol invariant — exact claims
and native verification under the same recursion system. The primitive
Multi-STARK and recursive-verifier suites were rerun to guard the shared
verifier and lift paths, and the separate activation gate covers 132 accepted
shape/input cases twice.

The focused ix-aggr command now runs 93 passing checks. Its 42
circuit/shape/plan checks cover both wraps, all flat and structural child-kind
pairs, a nested structural self child, generated/interpreter parity, strict
tree and native keyed-blob framing, identity/shape closure, wrong or tampered
paths, omitted carries, unsorted trees, flat↔structural hint confusion, a flat
parent fed a free-form structural root, and exact four-shard wrap-first/direct
plans. Its 51 converged semantic checks cover uniform claims, cache v2 key
invalidation and verified resume, recursion parameters, manifest
validation/pruning, singleton/canonical/hybrid value reconstruction,
shard-preparation closure sharing, wrap-first/direct planning, per-shape RAM
admission, dependency/failure behavior, and jobs=1/jobs=2 determinism including
concurrent zero-PoW proving. Rust unit tests separately pin channel-6 key layout
and strict path-blob framing.

Validation run

All of the following pass on the current stack:

nix develop --command lake exe IxTests aggregate-first # 57/57nix develop --command lake exe IxTests ix-aggr # 93/93nix develop --command lake exe IxTests aggregate-activation # 132 cases × 2 stable passesnix develop --command lake exe IxTests multi-stark # 12/12nix develop --command lake exe IxTests recursive-verifier # 6/6nix develop --command lake build IxTests bench-typecheck bench-recursion-debug bench-aggregate-policy ixnix develop --command cargo test -p ixvm-codegen aiur_multi_stark_runnernix develop --command cargo test -p ixvm-codegen aiur_ix_aggr_runnernix develop --command cargo test -p ix-kernel shard::tests # 23/23nix develop --command cargo test -p ixon test_check_env_claim_accepts_structural_rootnix develop --command cargo clippy -p ixvm-codegen -p ix-ffi -p ix-kernel -p ixon --tests -- -D warningsnix develop --command cargo fmt --all -- --checknix develop --command lake exe ix codegen --checkgit diff --check

Both benchmark entrypoints also reject --queries 0 with a usage error before
building or proving, and a stored pre-E2 proof now reports a checked decode
error rather than aborting. The fresh post-E2 aggregate fixture verifies
against both its .ixe and .ixes inputs.

A q=1 live smoke of the converged typecheck join also executed, proved, and
verified direct shape 2 successfully: 1.221742 s execution, 26,738,665,748 FFT
cost, 52.380108 s proving, 21,565,714,432-byte peak RSS, 401,340-byte proof,
and 0.002616 s verification. The M1-f policy harness passed plan-only smoke for
both policies and emitted the expected resumable JSON schema.

Pre-convergence real-environment gates on the 124 GB development host (kept
as WP-A/WP-B baselines for the M1-f rerun):

  • A two-shard manifest from the 5,986-constant tc-parity.ixe environment
    passes coverage and structural scheduling with threshold zero
    (2 lifts + 1 binary joins (1 structural)), and its one-shard variant
    plans 1 lifts + 0 binary joins.
  • Before E2, a one-constant q=100 production lift ran end-to-end through the
    cache and scheduler, establishing WP-A's invalidation/repair baseline:
PassResultWall
Initialproved and cached root 090bea6f…ca1535~64 s
Identical rerunverified cache hit, same root1.66 s
Truncated indexclean miss, re-proof, atomic repair, same root~38 s
Post-repair rerunverified cache hit, same root1.79 s
--jobs 2 --max-ram 400195 GiB reserved, verified hit, same root1.45 s

At scale, the measured pre-E2 4-shard Init run in Measured performance
above exercised the former backend with real shard proofs, four q=100 lifts,
two lower joins, and a root join with genuine assumption discharge. The pinned
M1-f harness will repeat this fixture under both converged policies.

Soundness summary

  • Every present child is fully verified under the verifying key selected by
    its shape-constrained kind.
  • Child function indices are constrained to IxVM verify_claim or ix_aggr,
    and every recursive child must carry the same allowed digest, pinning the
    two verifying keys and two entrypoints transitively.
  • Claims and allowed data are Blake3-bound and strictly parsed with no trailing
    bytes.
  • Flat mode reopens canonical roots and proves exact union/difference;
    structural mode proves the exact root-of-roots relation and accounts for
    every unique input assumption through either inclusion or explicit carry.
  • A bogus path, dropped candidate, extra survivor, malformed choice, or
    noncanonical assumption tree rejects. Over-carrying and duplicate subject
    leaves can only weaken the output claim; the CLI requires an unconditional
    final root.
  • Coverage is checked on the unmodified manifest, so pruning cannot conceal a
    hole or overlap; only leaves with an environment-derived owned-constant
    count of zero are removed, and contraction preserves the order and shape of
    retained leaves.
  • A singleton root is a verified shape-0 wrapper, never a raw-proof shortcut;
    the circuit binds its output digest directly to the pinned IxVM child's
    CheckEnv digest.
  • Manifest-aware verification independently repeats pruning, scheduling, and
    value folding, then verifies the one derived ix_aggr outer claim. No
    claimed proof kind or shape is accepted from wrapper metadata.
  • Proving and verification build the recursion system from the same explicit
    parameter object; any parameter change necessarily changes the recursion vk,
    allowed-system identity, and cache key.
  • Cache keys bind version, recursion-vk digest, explicit FRI bytes, and exact
    outer claim, all derived before proving; hits re-hash store content, bind
    the claim, decode without panicking, and verify natively. Corrupt, missing,
    or stale entries are misses, never trusted successes or command failures.
  • A pair is admitted only after both child slots complete; immutable
    snapshots and indexed installation make arrival order irrelevant. The
    controller owns all reservations; failures drain running work before exit.
  • Free-form structural roots are serialized as opaque commitments
    (crates/kernel/src/claim.rs); canonical openings are required only by
    protocol paths that explicitly need them.

Review map

Commit-by-commit review is recommended — each commit is feature-scoped with
its own tests (map in the first comment / commit list).

AreaPrimary filesWhat to review
Legacy regression protocolIx/MultiStark/Aggregate.lean, Ix/MultiStark.leanpre-convergence 96-byte identity and semantic/audit reference surface
Production circuitIx/Aggr/Circuit.lean, Ix/Aggr.lean80-byte identity, uniform claims, shapes 0–9, heterogeneous child pinning, structural path checks
Converged host modelIx/Aggr/Host.leanfree-form structural roots, canonical survivors, path extraction
Host statement modelIx/MultiStark/Host.leanfree-form subject roots, canonical assumptions, path extraction
Manifest parsing + pruningIx/Cli/CheckCmd.leanstrict tree parsing, validate-before-prune, unary contraction, dense remap
Aggregate driverIx/Cli/AggregateCmd.leanwrap-first/direct shape policy, singleton wrap, uniform slot claims, cache v2, per-shape RAM weights, verified DAG resume/failure draining
Root verificationIx/Cli/VerifyCmd.leanone ix_aggr outer claim, value-based environment comparison, manifest-relative hybrid reconstruction
Recursion parametersIx/MultiStark.lean, Ix/Aggr.lean, Ix/Cli/{Aggregate,Verify}Cmd.leanshared config plumbing, deterministic systems, stable FRI cache encoding
Pruned FRI multiproofsIx/Aiur/{Protocol,Semantics/BytecodeFfi}.lean, Ix/Cli/{Aggregate,Verify}Cmd.lean, crates/ffi/src/aiur/protocol.rscompact persisted proof vs expanded recursive advice boundary, checked legacy-proof failure
Cache/store boundaryIx/Store.lean, Ix/Aiur/Protocol.lean, crates/ffi/src/aiur/protocol.rswipeable namespace, checked proof decoding, corrupt-data fallthrough
Shard preparationIx/IxVM/ClaimHarness.lean, Ix/Cli/{Aggregate,Check,Verify}Cmd.leanclaim/tree-only consumers, one-pass multi-root witness closure
Native advicecrates/ixvm-codegen/src/aiur_multi_stark_runner.rsstrict framing and seven-channel key layout
Converged native advicecrates/ixvm-codegen/src/aiur_ix_aggr_runner.rs, crates/ffi/src/aiur/protocol.rsstrict path framing, shared channel-6 key layout, execute/prove passthrough
Generated executorcrates/ixvm-codegen/src/aiur_multi_stark.rsgenerated artifact; verify with ix codegen --check
Converged generated executorcrates/ixvm-codegen/src/aiur_ix_aggr.rsgenerated artifact; verify with ix codegen --check
Manifest writercrates/kernel/src/shard.rsproducer-side omission, tree remap, aggregate metadata
Claim semanticscrates/kernel/src/claim.rs, crates/ixon/src/proof.rsfree-form root contract and codec regression test
TestsTests/Aggr.lean, Tests/AggrSemantics.lean, Tests/AggrActivation.lean, Tests/MultiStark.lean93 production shape/semantic/plan checks, 132-case activation audit, retained Ix-agnostic regressions
BenchmarksBenchmarks/AggregatePolicy.lean, Benchmarks/Typecheck.lean, bench registry/dashboardspinned four-shard wrap-first/direct handoff, resumable JSON, converged direct-shape-2 join metrics, historical lift-size pins

Current limits and non-goals

  • Recursion FRI parameters stay at the q=100 defaults; the q=50 + compensating
    PoW policy is an explicit follow-up decision (the measured costs above are
    therefore the conservative end).
  • Structural roots are manifest-relative. Environment-only verification
    accepts only a bundled value equal to the canonical environment root;
    hybrid roots should be verified with both --ixe and --ixes and the
    proving threshold.
  • Scheduler RAM weights are calibration-pending placeholders (validated in
    the safe direction by the measured run); concurrent proves share the rayon
    global pool, so throughput can sub-scale — process-per-slot is a
    measurement-driven follow-up.
  • Same-key cross-process cache writes degrade to a warning and recomputation,
    never a soundness issue.
  • Dummy-call activation padding is deferred until/unless a static terminal
    circuit is selected; under the SP1 terminal it is pure cost.
  • Aggregate wrappers reuse Ixon.Proof and do not encode the proof system;
    callers must pass ix verify --aggregate.
  • M1-a–f land the converged production circuit, host/native substrate, driver,
    cache, scheduler, value-based verifier, semantic union, widened activation
    audit, and policy benchmark harness. The only remaining M1 work is executing
    the wrap-first/direct comparison on the target box. The pinned M1-f revision,
    inherited M1-e preflight, and both measured policies are one checkout/session;
    its handoff is documented in
    plans/aggregate-first-m1f-large-box-runbook.md.
  • This PR does not adopt cold-circuit grouping and does not implement a
    terminal KZG/SP1 wrapper.

Follow-ups

  1. Calibrate lift/join unit costs on a known-core box, with per-node
    flat-vs-structural attribution, and measure frontier/discharge-path counts
    at Mathlib scale (plan §11.4.1).
  2. Decide the recursion q=50 + PoW policy (§9.5) now that the mechanism
    exists.
  3. Connect the root proof to the selected terminal compression path (E3:
    single-recursion-proof SP1 guest + ix compress-root).
  4. Re-evaluate cold-circuit grouping (E4); E2's measured outer-lift reduction
    was only 6.80%, so grouping may still be material.
  5. Process-per-slot proving if measured rayon contention warrants it.
  6. Run the exact M1-f commit
    b70042d6d1a029bb8e1d541da7aef5e0613d67e6 for the combined M1-e preflight
    and wrap-first/direct policy benchmark session in the large-box handoff
    runbook.

johnchandlerburnhamand others added 22 commits August 27, 2026 16:21
Add the deterministic lift/flat/structural activation matrix and keep dummy calls deferred. Split aggregate recursion commitment/FRI configuration from IxVM defaults, share it between proving and verification, and pin the future cache encoding without changing active protocol parameters.
Precompute versioned per-slot cache keys, persist lift and join wrappers in the content-addressed store, and reuse entries only after exact claim and native outer-proof verification. Add safe proof decoding, corruption recovery, and --no-cache.
Execute ready lift and join slots as a dependency DAG under explicit job and RAM admission. Add calibration-pending slot weights, failure draining, CLI controls, and serial/parallel scheduler and proof-equivalence gates.
Add an opt-in two-child benchmark that proves singleton CheckEnv shards, lifts both proofs, and measures a verified flat join. Wire join metrics through reporting and dashboards, and record the current pre-E2 lift-size baseline.
- rust-toolchain.toml channel: 1.92 → 1.98, with the matching fenix
toolchain hash in flake.nix. The pinned fenix already carries the 1.98
release manifest, so flake.lock needs no change (fenix's nixpkgs stays
pinned via its lean4-nix follows, so the Lean toolchain is untouched).
- Drop clippy::from_iter_instead_of_collect from the workspace lints:
removed in clippy 1.98 and now warns as unknown.
- Fix the warnings new clippy 1.98 lints surface across the workspace:
chunks_exact(N) → as_chunks::<N>() where the chunk size is constant,
descending sort_by → sort_by_key(Reverse(..)), iteration over map
values via .values(), map().unwrap_or() → map_or(), a checked
division, an unwrap-after-is_some restructured into if-let, and
assorted redundant-reference/pattern cleanups (mostly cargo clippy
--fix). The two byte-gadget files keep their chunks_exact warnings
until the next commit, which rewrites those regions anyway.
CI derives its Rust version from rust-toolchain.toml, so no workflow
changes are needed.
Companion to multi-stark's update-p3 branch (c72d321 → 249b740), which
carries four soundness/robustness fixes and the Plonky3 v0.6.0 bump
(pruned FRI Merkle multiproofs: ~2x faster verification, 40-70% smaller
proofs; canonical Goldilocks serde removes proof-byte malleability).
Proofs and verifying keys are not compatible with the previous pin.
Integration:
- Lookup gained max_multiplicity, a declared per-row bound on the
multiplicity's integer magnitude feeding the newly enforced logUp
height bound Σ wᵢ·hᵢ + |claims| < p. Function-circuit slots accumulate
mutually-exclusive branch selectors, so they declare 1; committed
count columns (function return slots, the memory circuit, the byte
gadget tables) declare the new COUNT_COLUMN_BUDGET (2^32 queries per
entry).
- The VK wire format carries the bound: u64 LE max_multiplicity per
lookup, between the multiplicity node id and the arg count. The
in-circuit VK deserializer (Ix/MultiStark/SystemDeserialize.lean)
parses past it; the value is bound through the vk digest but the
height bound itself is not yet enforced in-circuit (the native
verifier enforces it).
- Message fingerprints are width-bound by default upstream (the slot
width seeds the Horner fold), which is incompatible with aiur's
branch-shared lookup slots: mutually exclusive branches superpose
messages of different natural widths into one slot at the maximum
width, so a narrow call is sent zero-padded to a width its callee's
return slot never provides, and proving fails with
UnbalancedChannel (pinned as the prove_verify_mismatched_call_widths
regression). Aiur instead declares WidthBinding::ByConstruction —
the plain Horner fold, restoring zero-padding transparency — and
takes on the prefix-freeness contract that makes it sound: every
message's natural width is a function of its constant-constrained
leading prefix (channel tag plus discriminator: fun_idx fixes
2+in+out, the memory size coordinate fixes 3+size, each gadget tag
fixes its table width), so zero-extension can only equate a padded
message with its own natural form. The contract is documented at the
channel constants in lib.rs; the declaration is applied in
AiurSystem::build and mirrored in the vk_codec decoder so decoded
VKs replay the same transcript. ByConstruction is also exactly the
fold the in-circuit verifier's logup_fingerprint already computes,
so the recursive verifier needs no fingerprint change.
- The policy is Fiat-Shamir-bound as the first observe_shape word; the
in-circuit transcript replay prepends the matching limb.
- aiur_multi_stark.rs regenerated (ix codegen) for the deserializer
and transcript changes.
- P3 v0.6.0 ships FRI query openings as pruned Merkle multiproofs,
while the in-circuit verifier consumes one authentication path per
query (its per-query control flow is a far smaller circuit than the
amortized multiproof walk). Rather than porting the walk into the
DSL, the proof advice stays in the per-query transport: multi-stark's
new advice module re-encodes a natively-verified proof by running
p3's own verification with a recording compression function and
reading each query's path back out of the recorded digest map. The
advice bytes are untrusted verifier input, never digest-bound —
the transcript binds the commitments and every expanded sibling is
authenticated against them per query — so pruning vs expansion is
pure transport and the encoding choice is sound. AiurSystem gains
proof_to_advice_bytes (FFI: AiurSystem.proofToAdviceBytes); the
recursive-verifier test feeds it instead of Proof.toBytes, whose
native wire format is still round-tripped separately. The Lean-side
proof grammar and the codegen'd verifier are byte-identical to
before — no in-circuit changes.
Claim layout, the VK wire format above and aiur's public semantics are
otherwise unchanged; the policy adds no prover or verifier work over
the previous pin.
Still open, native-verifier-only: the logUp height bound is parsed
past but not yet enforced in-circuit (a wide-arithmetic check, tracked
separately).
The Lean v4.33.1 update pinned both dependencies at revisions that
predate their Rust 1.98 bumps; their heads now carry those bumps, which
this workspace needs since rust-toolchain.toml moved to 1.98. Both
revisions stay on leanprover/lean4:v4.33.1.
- Blake3.lean 1b0fbd2 → e6e908b (Rust 1.98, plus a case-insensitive
source-directory fix), updated in lakefile.lean, lake-manifest.json
and the blake3-lean flake input. The revision keeps the
`blake3_rs_shared` target the `ix_native_decide_dynlib` pin requires.
The inherited entry in Benchmarks/Compile/lake-manifest.json was still
on the pre-4.33.1 revision and now tracks the root pin.
- lean-ffi 2a9c91e → 93c7e52 (Rust 1.98). Only bignat reaches the sp1
and zisk workspaces, so their lock files move that one package.
The !benchmark recursive phase reported n/a for every fri-verifier
metric: Benchmarks/Typecheck.lean still fed Proof.toBytes — the pruned
multiproof wire format — to executeMultiStark/proveMultiStark, so the
in-circuit verifier rejected on parse and the harness (correctly) left
the recursive fields absent rather than emit a fake datum. The
in-circuit verifier consumes the per-query advice transport
(AiurSystem.proofToAdviceBytes); proofBytes stays the reported
proof-size metric.
bench-recursion-debug had the same advice-format gap plus a stale
claim recipe: it still built the public input as 32 raw digest bytes,
predating the ClaimHarness.packedDigestKey packing bench-typecheck
uses (its own out-of-circuit sanity check failed with
InvalidPowWitness — a wrong claim diverges every challenge — and the
advice re-encoder refused the proof for the same reason). Both aligned
with the typecheck flow.
Validated end-to-end at production parameters (numQueries 100,
query PoW 20, blowup 2) on Nat.add_comm: inner prove, advice
re-encoding, and the codegen'd in-circuit verifier accepting.
Keep compact proof bytes at storage and cache boundaries, expand them only for recursive lift/join advice, reject unsupported zero-query benchmarks, and decode legacy store proofs without panicking. Refresh tests, benchmark pins, and Rust 1.98 lint compatibility.
Add Ix/Aggr, a recursive aggregation system for IxVM shard proofs that
keeps Ix/MultiStark untouched and Ix-agnostic. One entrypoint, ix_aggr,
subsumes lifting and joining: a one-byte advice shape selects wrap or
binary join over any mix of IxVM and ix_aggr children, so shard proofs
enter the recursion system directly as join children and the dedicated
lift stage disappears.
Circuit (Ix/Aggr/Circuit.lean): every shape verifies its children in
full (verify + ood_verify from the shared Multi-STARK verifier modules)
against the vk its hinted kind demands. Identity is one 80-byte
digest-bound blob - blake3(ixvm vk) || verify_claim idx ||
blake3(self vk) || ix_aggr idx - carried unchanged at every node; self
children must bind the identical blob digest, pinning both vks and both
entrypoint indices transitively. Claims are a uniform 18-word
[0, aggr_idx, allowed(8), checkEnv(8)] at every depth, so proofs of
different tree levels combine freely. Wrap shapes bind the output
digest to the child CheckEnv digest directly; pair shapes open both
CheckEnv preimages, re-root the canonical subject/assumption trees, and
prove subjects = L ∪ R, assumptions = (asmL ∪ asmR) ∖ subjects with
linear sorted merges.
Toplevel (Ix/Aggr.lean): ixAggr = MultiStark.multiStarkFull + circuit,
pruned to ix_aggr, so verify_multi_stark_proof and other unrelated
entries no longer pad aggregate proofs. The host half of the wire
contracts (allowed blob, public input packing, shape codes, keyed
preimage/tree blob framing, interpreter IO assembly) lives beside the
toplevel; Ix/Aggr/Host.lean folds CheckEnvTrees statements.
Native path: ix codegen gains the ix-aggr target
(crates/ixvm-codegen/src/aiur_ix_aggr.rs, 244 fns); its runner builds
the seven-channel IO buffer natively and routes execution through the
generated code. New FFI rs_aiur_ix_aggr_execute/_prove
(executeIxAggr/proveIxAggr) pass proofs, vks, claims, and the compact
count/key/length preimage/tree blobs without per-byte boxing, plus
Proof.ofBytesChecked for store-boundary decoding.
CLI: ix aggr --ixe E --ixes M <shard-proof>... reconstructs every
nonempty shard statement from the env, matches wrappers by claim
digest, natively pre-verifies them, folds a balanced bisection (the
canonical fold makes the root claim independent of tree shape), wraps
single-shard roots so the persisted root is always an ix_aggr proof,
checks the root closes over the env canonical tree with no residual
assumptions, and persists the wrapper.
Tests (lake test -- ix-aggr, 17 cases): all five shapes accept over
real Multi-STARK stand-in child proofs from two distinct-vk systems;
codegen'd execution matches the interpreter on output and per-circuit
query counts for wrap and pair; negatives break one binding each -
lying shape hint, tampered proof, foreign identity, wrap statement
drift, dropped assumption, padded subject set, and tree advice not
reproducing its keyed root.
Replace the legacy lift/join entrypoints with the heterogeneous ix_aggr system across aggregate, verification, cache, and codegen paths. Preserve wrap-first and direct-join policies behind one proof identity and cache namespace.
Add the converged 91-check semantic suite and a deterministic 132-case activation audit covering shapes 0 through 9 twice, with no unobserved circuits.
Replace the legacy two-shard three-entrypoint benchmark with a four-shard manifest-subtree harness for production ix_aggr wrap-first and direct policies. Enforce the q=100 serialized no-cache profile, natively verify every input and recursive output, persist ordinary aggregate wrappers, and emit resumable per-slot JSON metrics.
Port bench-typecheck --join to ix_aggr direct shape 2 while preserving the stable join metric schema. Remove the final legacy slot-spec/preimage shim and pin both M1-f four-shard plans in the focused suite.
Update the multi-stark dependency and Rust toolchain for Plonky3 0.6, along with the Rust 1.98 lint migrations required to keep the workspace warning-free. Refresh the Rust-compatible Blake3.lean pin in both root and compile-package manifests.
Adapt recursive Aiur verification to Plonky3's pruned FRI multiproofs. Native proofs retain their compact serialized representation and native verification path; the FFI expands authenticated Merkle frontiers into per-query advice only when entering the existing recursive verifier circuit.
Preserve the packed claim-digest convention in the recursion diagnostic and exercise the advice boundary in the end-to-end test and benchmark paths. CPU and CUDA recursive q1 runs produce identical 823,485-byte inner proofs and 331,273-byte outer proofs.
The q50 Vector.extract_append workload retains identical CPU/CUDA proof sizes. Inner plus outer STARK proving measures 65.87s on CPU and 8.81s with CUDA on the RTX PRO 6000, a 7.48x speedup.
PR benchmark runs execute trusted workflow YAML from the default branch while loading composite actions from the PR checkout. When Bencher data and binary caches are unavailable, the workflow checks out main under base/ and asks Lake to rebuild it without first installing the Rust channel pinned by that checkout.
Teach the existing CPU provenance action to install the base checkout's validated Rust channel and profile immediately before an uncached base build. The step is a no-op when the toolchain is already available and leaves cached benchmark comparisons unchanged.
Consume the Plonky3 0.6 batch-opening layout directly in Aiur instead of expanding every pruned Merkle frontier into one authentication path per FRI query. Sample all query indices from the unchanged transcript, sort and deduplicate them with an O(q log q) merge sort, authenticate each input and commit-phase commitment once, then retain the existing per-query reduced-opening and FRI arithmetic.
Bind every frontier to transcript-derived indices, consume boundary digests in Plonky3's level/parent/child order, reject trailing frontier elements and inconsistent duplicate leaves, and assert all native opening dimensions and sibling counts. Explicitly constrain the digest-bound protocol specialization to cap height 0, binary FRI, and a constant final polynomial. Move memo_u32_less_than into IxVM Core so both substitution and multiproof sorting share its constrained rows.
Strengthen the recursive negative test to mutate a structurally valid stage-1 commitment. Regenerate both checked-in Aiur Rust executors and retain interpreter/codegen query-count parity.
On Vector.extract_append q50, recursive-verifier FFT cost falls from 204.073B to 201.166B. CPU outer proving improves from 50.09s to 45.03s and the full CPU pipeline from 90.64s to 82.90s. GPU outer proving improves from 15.85s to 13.72s and the full GPU pipeline from 28.86s to 26.69s. The outer proof grows from 3.92 MB to 4.17 MB.
Validated with the MultiStark primitive suite, recursive honest/tamper/parity tests, codegen --check, release workspace clippy, release CUDA clippy, rustfmt, and diff checks.
@johnchandlerburnham

Copy link
Copy Markdown
MemberAuthor

Init full proving report

Revision: 82dcc9dba36d0cfd6c753c6878ab649643a52026
Host: 64 vCPU, 495 GiB RAM, 256 GiB swap
Final result: one verified ix_aggr FRI proof with no remaining assumptions.

Compilation and sharding

StepResult
Compile sourceBenchmarks/Compile/CompileInit.lean
Ix compiler time1.876s
Constants compiled65,994
init.ixe size194,296,853 bytes (185.3 MiB)
Static sharding time3.1s
Manifest16 nonempty shards
Retained/owned constants56,621
Manifest size2,619,404 bytes (2.50 MiB)
Predicted FFT per shard4.160e11–4.197e11
Predicted FFT spread1.01×

The 1.876-second compile measurement is the Ix compiler itself; it excludes the initial one-time dependency download and Lake build.

Stage 1: base shard proofs

All 16 shard proofs were persisted and individually verified against their corresponding manifest claims.

MetricSerial baselineTwo-way run used for aggregation
Concurrency12
End-to-end wall time24m 16.76s15m 53.93s
Sum of shard prover times22m 43.23s30m 08.14s
Per-shard time range74.08–113.97s98.94–142.29s
Median shard time82.66s108.75s
Highest individual RSS219.8 GiB219.8 GiB
Combined process-tree peak358.2 GiB
Proof count1616
Total proof-wrapper size369,995,038 bytes
Mean proof-wrapper size23,124,690 bytes
Proof-wrapper range22,714,520–23,998,692 bytes

Two-way concurrency gave a 1.53× wall-time speedup despite increasing total prover-seconds by about 33% from CPU/memory contention.

Stage 2: recursive aggregation

Policy:

  • Wrap-first
  • --structural-above 4096
  • --jobs 2
  • --max-ram 450
  • Default q=100 recursion parameters
  • Aggregate cache enabled

Plan:

OperationCount
Shard wraps16
Binary joins15
Structural joins14
Flat joins1
Total recursive slots31

Results:

MetricResult
Wall time45m 06.61s
Peak resident memory379.9 GiB
Average CPU utilization4,192% (~42 cores)
Cache hits0
Freshly proved/cached slots31
Swap used0 bytes
Exit status0
Final proof-wrapper size9,596,044 bytes (9.15 MiB)

Final proof

Root address:

80f6844f968920abaef8d8c750a1a1dc1ebe32657a3cfb824e1810bcd65c8814

Final claim:

CheckEnv(6dc8f821a989c6f7b9d520b053ef8edbd582d2408d7b6a6ff72b908666e03b6f, none)

Both validations passed:

  • Cryptographic-only aggregate verification
  • Manifest-aware verification against init.ixe, init-16.ixes, and structural threshold 4096

End-to-end measured time

Using the two-way Stage 1 run:

ComponentTime
Ix compilation1.876s
Sharding3.1s
Stage 1 proofs15m 53.932s
Stage 2 aggregation45m 06.61s
Measured core pipeline1h 01m 05.52s

This total excludes the one-time dependency build/download and a few seconds of manifest and final-root verification overhead. Using serial Stage 1 instead would make the measured core pipeline approximately 1h 09m 28.35s.

Use the PR77 lean-ffi and multi-stark pins, remove the stale width-binding and multiplicity-bound protocol fields, and regenerate the recursive and aggregate verifier sources.
Expose an opt-in x86_64-linux cuda shell with NVCC 13.2, sm_120 code generation, and safe host libcuda discovery while leaving the default development shell unchanged.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@johnchandlerburnham@samuelburnham@arthurpaulino
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root - #598

Open
johnchandlerburnham wants to merge 24 commits into
mainfrom
jcb/aggregate-first
Open

Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root#598
johnchandlerburnham wants to merge 24 commits into
mainfrom
jcb/aggregate-first

Conversation

@johnchandlerburnham

@johnchandlerburnhamjohnchandlerburnham commented Aug 27, 2026

Copy link
Copy Markdown
Member

Summary

This PR implements the aggregate-first path for Aiur Multi-STARK proofs. The
production path now runs through one ix_aggr recursion entrypoint: it wraps
independently-proven IxVM shards, folds their CheckEnv statements along the
manifest's bisection tree, and persists one recursive root proof with a
uniform 18-word outer claim. Every completed recursive slot is persisted in a
verified, content-addressed version-2 resume cache, and ready slots are proven
in parallel under explicit job and RAM limits.

The entrypoint has two pair-fold modes:

  • Flat shapes perform a canonical set union/difference and are efficient for
    small lower nodes.
  • Structural shapes commit to nodeHash(leftRoot, rightRoot) in one hash and
    discharge assumptions with Merkle inclusion paths. Their 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 (default
4096 subject leaves): flat below the threshold, structural above it.

IxVM shard proofs
│
├── ix_aggr shape 0 (default wrap-first policy)
│ or remain raw with --direct-joins
▼
ix_aggr proofs carrying uniform CheckEnv statements
│
├── flat ix_aggr shapes 2–5 below the configured subject threshold
│ subjects := canonical(subjectsL ∪ subjectsR)
│ assumptions := (assumptionsL ∪ assumptionsR) ∖ subjects
│
└── structural ix_aggr shapes 6–9 above the threshold
subjects := nodeHash(subjectRootL, subjectRootR)
assumptions := candidates carried or discharged by inclusion path
▼
Ixon.Proof {
claim := CheckEnv(manifest-relative subject root, none),
proof := one recursive Multi-STARK root proof
}

A manifest with one retained (nonempty) shard is also a valid deliverable: its
root is one shape-0 wrapper, with no self-join padding (including under
--direct-joins). Zero-constant manifest leaves are pruned after raw coverage
validation:

raw manifest
|
| validate exact environment coverage before pruning
v
drop zero-constant leaves + contract unary tree nodes + remap retained ids
|
+-- one retained leaf --> wrap once and persist the ix_aggr root
|
`-- multiple leaves --> fold flat/structural joins as above

This is the stage-2 aggregation layer from
plans/aggregate-first-pipeline.md: shard proofs are aggregated before any
terminal KZG/SP1 compression, so a later wrapper only needs to consume one root
proof. Terminal compression remains outside this PR.

Section 14 implementation is complete through M1-f in this branch. The
ap/ix-aggr single-entrypoint base is reconciled with WP-E2's compact stored
proof / expanded recursive-advice boundary, and its one ix_aggr entrypoint
supports wraps, flat pairs, and structural pairs across both IxVM and recursive
children. ix aggregate now uses that backend for planning, proving, cache
resume, and final verification; ix verify --aggregate derives one uniform
outer claim and performs value-based environment/manifest checks. The old
ix aggr command and all three-entrypoint production wiring are retired. The
older Multi-STARK aggregate implementation remains as an Ix-agnostic regression
surface, not as a CLI backend. Production semantics, activation coverage, and
benchmarking now live entirely against ix_aggr. M1-f lands the pinned
four-shard bench-aggregate-policy handoff and ports the stable typecheck join
metric to direct shape 2; only execution of the policy comparison on the large
box remains. Operationally this is one future handoff: Arthur will check out
b70042d6d1a029bb8e1d541da7aef5e0613d67e6 once, run the inherited M1-e
correctness suite as untimed preflight, and then benchmark both policies. There
is no separate M1-e box run.

Measured performance (2026-08-29)

Arthur Paulino ran the pre-convergence pipeline end-to-end on four real Init
shard proofs (shards 8–11 of a 16-way partition, 12,493 constants; base shard
proving excluded; native verification of each output included; ~512 GB box).
These are the standing M1-f comparison baseline; the converged wrap-first and
direct policies have not yet been rerun on that box. The recursion used the
conservative q=100 / PoW 20 defaults this PR keeps:

StageTimePeak RAMOutput proof
Lift × 498.8–103.0 s (mean ≈ 101 s)186.8–195.8 GiB8,162,462 B each
Join 8+9, 10+1148.9 / 48.5 s~102.5 GiB9,502,843 B
Root join85.8 s156.9 GiB9,455,498 B
Total (serialized)10:23 wall195.8 GiB9,455,498 B

Notes: lift cost is shard-content-independent (four lifts within ±2%, tracking
the ~constant 20–23 MB shard-proof shape); the measured lift peak brackets the
scheduler's 195 GiB weight placeholder exactly; and an unserialized run of all
four lifts OOM'd at ~492 GiB — precisely the behavior the WP-B admission gate
prevents (on a 512 GB box its weights admit two lifts). Because shards 8–11
are a strict partition subset, the root carries a real frontier assumption
set, so the joins exercised genuine discharge work. Full analysis, including
the comparison against direct (unlifted) IxVM joins, is in
plans/aggregate-first-pipeline.md §3.4 and the Zulip thread.

What lands

Protocol / circuit layer:

  • One production recursion entrypoint, ix_aggr, under one verifying key.
    Shapes 0–1 wrap one IxVM/recursive child, shapes 2–5 fold flat pairs, and
    shapes 6–9 fold structural pairs for every child-kind combination.
  • Recursive validation of wrap, flat-pair, and structural-pair children with
    transitive verifying-key, function-index, and allowed-system binding. A
    shape hint selects the verified form but cannot weaken its checks.
  • Strict in-circuit CheckEnv decoding and canonical flat set folding.
  • Structural subject folding with per-candidate Merkle discharge paths while
    keeping output assumption sets canonical and deduplicated.
  • One 80-byte protocol identity containing the IxVM/self verifying-key
    digests and their two accepted entrypoint indices; every recursive node has
    the same 18-word outer-claim layout regardless of shape.
  • Structural outputs bind nodeHash(leftRoot, rightRoot) and account for each
    unique assumption candidate through a strict Merkle-path-or-carry choice.
  • Host helpers construct structural roots, survivors, and path advice. Channel
    6 carries both the one-byte shape at key [0] and candidate path payloads at
    raw 32-byte address keys; the key shapes are disjoint.
  • Lean/Rust execute and prove FFI accept a compact keyed path blob, whose
    framing is decoded strictly before the circuit independently validates
    payload semantics.
  • The generated ix_aggr executor is regenerated at 2,061,361 bytes / 248
    Aiur functions and parity-tested against the interpreter.

Host driver and operations:

  • Strict .ixes aggregation-tree parsing and post-order lowering, with a
    balanced fallback for legacy manifests; validate-then-prune removal of
    zero-constant leaves with unary tree contraction (and a Rust writer that
    stops emitting them).
  • Single-shard roots: a one-retained-shard manifest always produces one
    shape-0 ix_aggr wrapper, including under --direct-joins.
  • A monotone leaf-count scheduler controlled by --structural-above, with a
    default wrap-first plan (shapes 0 then 5/9) and an explicit
    --direct-joins plan that derives heterogeneous shapes from actual child
    kinds.
  • Versioned, content-addressed aggregate cache and resume (WP-A): version 2
    binds the uniform outer claim, and reuse occurs only after content-digest,
    claim, decode, and native verification all pass; --no-cache bypasses.
  • Parallel execution of ready wraps/pairs as a dependency DAG under --jobs
    and --max-ram (default 92% of MemTotal), heaviest-first, with failure
    draining (WP-B). Admission weights are shape-aware: 4 GiB for a retained
    raw IxVM leaf, 195 GiB for wraps/self pairs, 390 GiB for IxVM/IxVM pairs,
    and 340 GiB for mixed pairs.
  • A recursion-parameter split (WP-E1): aggregate proving/verification share an
    explicit RecursionParameters value, defaulting to today's q=100 values;
    the q=50+PoW policy decision is deliberately deferred.
  • A deterministic 132-case production activation audit across shapes 0–9:
    two controlled child trace heights, both wraps, every flat/structural
    child-kind pair, optional assumptions per side, and discharge/carry. The
    complete matrix runs twice with a stable signature and observes every
    catalogued circuit; dummy-call padding remains intentionally deferred.
  • Claim-only shard preparation (no discarded dependency-byte closure walks)
    with a one-pass multi-root witness closure for proving.
  • ix aggregate --ixe E --ixes M [--structural-above N] [--direct-joins] [--jobs N] [--max-ram G] [--no-cache] [--plan-only] <proof>... and
    single-entrypoint aggregate verification with cryptographic-only,
    environment-bound, and manifest-bound value checks. The duplicate
    ix aggr command is removed.
  • Opt-in aggregate benchmarks: a tiny two-shard Init pair end-to-end bench and
    a flat-join bench (singleton CheckEnv shards → two lifts → verified flat
    join), with join metrics wired into reporting/dashboards, the pre-E2 lift
    proof size pinned at 7,986,166 bytes, and the post-E2 delta measured.
  • Positive, transitive-recursion, scheduler, cache, pruning, parity,
    malformed-input, and semantic-tampering tests: the aggregate-first suite
    has 58 cases, plus the separate activation matrix.

Protocol design

One recursion system, one entrypoint

Production builds Aggr.ixAggr, the shared Multi-STARK verifier closure pruned
to one ix_aggr entrypoint. A one-byte advice hint selects the exact verified
shape:

ShapeChildrenFold
0IxVMpass-through wrap
1ix_aggrpass-through wrap
2–5IxVM/ix_aggr pair (2 + 2·left + right)canonical flat fold
6–9IxVM/ix_aggr pair (6 + 2·left + right)structural fold

The hint is advice, not authority: each arm verifies the child proof(s) under
the key required by the encoded child kinds, then requires that kind's exact
claim layout. A wrong shape therefore fails proof verification, function-index
binding, or claim decoding.

Uniform public statement and pinned protocol identity

Every wrap and pair exposes the same public-input shape:

ix_aggr(allowed_digest: [G; 8], out_claim_digest: [G; 8])

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:

  1. the 80-byte allowed-system blob; and
  2. the serialized output CheckEnv claim.

The allowed blob is:

blake3(ixvm_vk)
|| verify_claim_idx:u64le
|| blake3(ix_aggr_vk)
|| ix_aggr_idx:u64le

The circuit requires exactly 80 bytes. 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
another function in those systems; binding the two accepted 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.
Every persisted aggregate outer claim is consequently the same 18-word form:
the ix_aggr function selector followed by the packed identity and output
claim digests. Shapes are witness choices and do not become root-kind metadata.

Child-proof validation and transitive pinning

Each pair reads the verifying key required by each child kind, checks its
Blake3 digest against the allowed blob, and deserializes it strictly. Wraps do
the same for their one child. For every child the selected arm:

  1. reads and fully consumes one proof and exactly one outer claim;
  2. runs the Multi-STARK structural verifier;
  3. runs ood_verify, including Fiat-Shamir replay, lookup accumulators,
    quotient/OOD checks, Merkle openings, and FRI verification; and
  4. decodes the statement according to the constrained child kind and function
    index.

The accepted forms are:

  • IxVM child: its system digest must equal blake3(ixvm_vk). Its claims
    digest opens to exactly one 10-word IxVM claim at the pinned
    verify_claim_idx, whose digest opens to a strict serialized CheckEnv
    claim.
  • ix_aggr child: its 18-word claim must use the pinned ix_aggr_idx; its
    first public digest must equal the current allowed_digest, transitively
    pinning both systems and both entrypoints; and its second digest opens to its
    output CheckEnv claim.

Any other index, shape, digest, trailing byte, or failed proof check rejects.
This keeps wrap→pair, flat→pair, and structural→structural composition on one
statement format.

Flat canonical folding

For child statements (S_L, A_L) and (S_R, A_R), flat shapes 2–5 enforce:

S_out = S_L ∪ S_R
A_out = (A_L ∪ A_R) ∖ S_out

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 u32 words using the
full-domain u32_less_than primitive. The implementation never uses Aiur
pointer 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

Structural shapes 6–9 perform the same allowed-blob binding, child-key
binding, two child-proof verifications, child decoding, and output-claim
binding as the flat shapes. They replace all subject-tree loading and set
re-rooting with:

S_out.root = nodeHash(S_L.root, S_R.root)

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:

  • carried (0): no bytes may follow, and the candidate must be the next
    value in the output assumption list; or
  • discharged (1): a bounded Merkle path from
    leafHash(candidate) to S_out.root must verify.

Path payloads are strict:

0
1 || count:u8 || repeated(side:u8 || sibling[32])

count is limited to 64. side = 0 hashes nodeHash(sibling, current) and
side = 1 hashes nodeHash(current, sibling); any other side rejects. The
payload 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

All ix_aggr shapes use the same seven-channel IO layout. Digest/root bindings
are checked before the corresponding bytes are decoded.

ChannelKeyValue
0[0], [1]first and optional second child proof advice bytes
1[kind]verifying-key bytes (0 = IxVM, 1 = ix_aggr)
2[0], [1], [2]left/right outer claims and output CheckEnv claim
3[0]80-byte allowed-system blob
4packed Blake3 digestnested claim preimages
5raw 32-byte addressserialized canonical trees keyed by root
6[0] / raw candidate addressshape byte / carried-or-discharged path choice

Lean passes channels 4–6 across FFI as compact framed blobs:

count:u32le || repeated(key[32] || payload_len:u32le || payload)

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.

Wraps receive no tree or path advice. Flat pairs receive full
subject/assumption tree advice and an empty path blob. Structural pairs receive
only the input/output assumption trees plus one path choice for every
deduplicated candidate; subject trees are never opened.

Manifest and host pipeline

.ixes aggregation tree

parseIxesManifest exposes both shard block lists and the optional binary
aggregation tree stored at the manifest tail. It:

  • requires nonzero, contiguous shard IDs;
  • validates option and tree tags;
  • requires tree leaves to be exactly the manifest shard set;
  • rejects duplicate/out-of-range leaves and trailing bytes; and
  • synthesizes a balanced ascending-ID tree for legacy manifests without a
    tree tail or with an explicit absent-tree tag.

The tree lowers to post-order FoldOp slots. Every join refers only to earlier
slots and the last slot is the root, providing a direct serial schedule and the
basis for cache keys and parallel execution.

Validate first, then prune empty manifest leaves

IxesManifestView retains the original shard id for every dense shard slot.
pruneEmpty:

  1. counts the environment constants owned by every raw shard;
  2. drops only shards with a count of zero;
  3. contracts aggregation-tree nodes with a removed child;
  4. densely remaps retained leaf indices; and
  5. returns the retained per-shard subject counts with the pruned view.

The aggregate and manifest-aware verifier paths deliberately call
shardsCover on the raw manifest before this transformation. Coverage
still establishes that every environment constant is owned exactly once. The
subsequent zero count is therefore evidence that a removed leaf contributes no
subject; pruning is not allowed to hide a missing or duplicated constant.

Original shard ids are preserved separately from dense scheduling ids, so CLI
plans and proof-binding errors continue to identify the source manifest shard.
An all-empty manifest is rejected because it cannot produce a root.

For a tree node node(left, right), pruning follows:

both retained -> node(left', right')
only left retained -> left'
only right retained -> right'
neither retained -> removed

The Rust .ixes writer (crates/kernel/src/shard.rs) also normalizes new
manifests before serialization — block-empty shard records are omitted, ids
rewritten densely, the tree pruned/contracted/remapped, and cross-ingress
recomputed. The Lean-side pass remains necessary for legacy manifests and for
the stronger environment-relative condition (a shard may contain blocks yet
own zero constants).

Monotone structural scheduling

The host counts actual owned constants for each shard after the disjoint-cover
check. schedulePlan annotates every slot with its cumulative subject count and
chooses a structural pair exactly when:

left.subjectCount + right.subjectCount > structuralAbove

The default threshold is 4096; --structural-above 0 makes every join
structural 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.

By default, every leaf is an ix_aggr shape-0 wrap, so binary slots are shape
5 below the threshold and shape 9 above it. With --direct-joins, a
multi-shard plan keeps leaves as raw IxVM children and derives each pair shape
from its actual child kinds: 2–5 for flat pairs or 6–9 for structural pairs.
The monotone threshold rule is unchanged.

The proving loop checks reconstructed subject counts against the schedule
before starting expensive work.

Singleton roots

ix aggregate imposes no two-shard minimum. The fold plan for one retained
leaf contains one shape-0 wrap and zero pairs; that wrapper and its CheckEnv
claim are persisted as the aggregate root. Even --direct-joins wraps a
singleton because the deliverable must verify under the one production
entrypoint. A self-pair would add work and manufacture a different statement.
Consequences:

  • exactly one proof is required per nonempty shard;
  • --plan-only reports one wrap and zero binary pairs for a singleton;
  • multi-shard flat/structural scheduling is unchanged; and
  • the final unconditional-root check always applies to an ix_aggr root.

ix aggregate

ix aggregate --ixe ENV.ixe --ixes SHARDS.ixes \ [--structural-above N] [--direct-joins] [--jobs N] [--max-ram G] [--no-cache] \ SHARD_PROOF_ADDR...

The command:

  1. deserializes the environment and manifest and verifies a disjoint exact
    shard cover on the raw manifest, then prunes zero-constant leaves;
  2. reconstructs every retained shard's exact CheckEnv claim and canonical
    trees (claim-only preparation — no dependency-byte closure);
  3. matches proof arguments to shards in any order by bundled claim digest,
    then requires exact claim equality and one proof per nonempty shard;
  4. natively verifies every IxVM shard proof before expensive recursion work;
  5. builds the IxVM and ix_aggr recursion systems and the pinned 80-byte
    allowed blob;
  6. derives every slot's statement, child kind, shape, uniform outer claim, and
    version-2 cache key up front;
  7. executes ready wraps and flat/structural pairs as a RAM-gated dependency
    DAG, resuming verified cached slots and persisting each completed wrapper
    (raw direct-policy leaves are verified inputs, not cache slots);
  8. canonicalizes all leaves under the final subject tree and checks that they
    reproduce the environment's canonical root;
  9. requires no remaining assumptions and natively verifies the outer proof at
    the one ix_aggr root entrypoint; and
  10. persists an Ixon.Proof containing the output CheckEnv claim and proof.

--plan-only stops after coverage validation and scheduling, resolving and
printing the scheduler policy without loading proofs or compiling recursion
systems:

ix aggregate --ixe ENV.ixe --ixes SHARDS.ixes \ --structural-above 0 --plan-only

ix verify --aggregate

Aggregate roots reuse Ixon.Proof, so verification is selected explicitly.
The verifier never accepts root-kind metadata. It serializes the bundled
CheckEnv value and reconstructs the only valid outer claim:

claim_bytes := serialize(CheckEnv(...))
outer_claim := ix_aggr(
blake3(allowed_blob),
blake3(claim_bytes))
InvocationCryptographic entrypointExpected statement value
ix verify --aggregate <proof>ix_aggrbundled claim; cryptographic proof only
ix verify --aggregate --ixe E <proof>ix_aggrcanonical environment root, unconditional
ix verify --aggregate --ixe E --ixes M <proof>ix_aggrpruned manifest fold, unconditional

Environment-only mode compares the bundled value to the canonical environment
root; a manifest-relative structural value therefore rejects naturally, with
no root-kind branch. Manifest-aware verification repeats pruning and the fold
schedule (the threshold must match the proving run), reconstructs every leaf
statement, folds the exact hybrid value, requires no assumptions, and verifies
the same ix_aggr outer claim. Existing non-aggregate proof and
shard-composition verification paths are unchanged.

Claim-only shard preparation

shardCheckEnvClaimTrees constructs only the CheckEnv claim and its
canonical subject/assumption trees. Aggregate planning, shard-digest
reconstruction, and aggregate verification use this path because none of them
consumes dependency bytes. shardCheckEnvClaim remains the witness-building
entry point; it reuses the same claim/tree result and computes the byte
closure once from the union of the owned and available primitive roots.
Reachability distributes over that root union, so this preserves the old
closure while avoiding repeated traversal of shared subgraphs.

Recursion parameters (WP-E1)

Aggregation and aggregate verification consume one shared
MultiStark.RecursionParameters value containing the recursion commitment and
FRI configurations, carried through runAggregateCmdWith/runVerifyCmdWith
into the shared MultiStark.buildRecursionSystem. The CLI entrypoints pass
defaultRecursionParameters, whose fields are exactly today's canonical Aiur
defaults. This is a mechanism split, not a policy change:

  • ordinary IxVM proving and verification remain on the canonical q=100
    configuration;
  • recursion proofs also remain q=100 with the same commitment and PoW values;
  • no new CLI flag selects security parameters; and
  • choosing q=50 plus a compensating PoW policy remains the explicit §9.5
    follow-up decision.

The recursion verifying key serializes and binds both parameter sets, and its
digest is included in every aggregate node's allowed-system blob, so any parameter
change changes the recursive protocol identity without a public-input or
allowed-blob format change. RecursionParameters.cacheFriBytes pins the
40-byte fri_params_ser cache-key component:

logFinalPolyLen:u64-le || maxLogArity:u64-le || numQueries:u64-le ||
commitProofOfWorkBits:u64-le || queryProofOfWorkBits:u64-le

Commitment parameters need no second cache component: changing them changes
the recursion-vk digest already present in the key.

Pruned FRI multiproofs (WP-E2)

The four-commit origin/update-multi-stark series is transplanted onto this
stack: Rust 1.98, the 249b7405 multi-stark revision on P3 v0.6, the matching
Blake3/Lean-FFI pins, and recursive proof-advice encoding. The new proof wire
format deduplicates shared Merkle siblings across sampled FRI queries. Width
and trace-height binding and the vk's checked max_multiplicity travel with
the dependency update; no aggregate claim or public-input format changes.

There are now deliberately two proof encodings. Proof.toBytes is the compact
wire form used by wrappers, native verification, the store, and WP-A cache
entries. Recursive execution still expects one path per query, so callers use
AiurSystem.proofToAdviceBytes immediately before a wrap or pair. The CLI,
pair/typecheck benchmarks, semantic tests, and activation audit all enforce
that boundary. Cached proofs stay compact at rest and are expanded only when
consumed as a parent pair's child.

The dependency's compact format is not backward-decodable. Store-facing CLI
verification therefore uses Proof.ofBytesChecked and reports a normal error
for legacy or corrupt proofs instead of crossing the unchecked decoder and
aborting. P3 v0.6 also requires a positive FRI query count; benchmark CLIs
reject --queries 0, and the old q=0 join smoke gate is replaced by q=1.
The Rust 1.98 migration also applies its new strict-Clippy equivalents for
fixed-size digest chunking and fallible metadata-size lookup; these are
mechanical, semantics-preserving rewrites.

Verified cache and resume (WP-A)

Before the first proof, the driver derives every fold slot's CheckEnv
statement, exact outer Aiur claim, and cache key, using the same
flat/structural fold policy as proving — so a manifest rebudget or statement
change invalidates only the changed subtree. The key:

blake3(
aggregateCacheVersion:u64-le ||
blake3(recursion_vk) ||
recursion_fri_params:40-bytes ||
serializeClaims([outer_claim])
)

The uniform outer claim already includes the ix_aggr entrypoint index and
public input, hence the allowed-system blob, output CheckEnv, both
verifying-key identities, and both accepted entrypoint indices. Each completed
recursive slot is stored as an Ixon.Proof wrapper in the content-addressed
~/.ix/store; a raw --direct-joins leaf is only an input and is not cached.
The wipeable index ~/.ix/cache/aggregate/<key> holds only the wrapper's store
address and is updated via temp-file + atomic rename. M1-d bumps
aggregateCacheVersion from 1 to 2, so old outer-claim entries miss cleanly.

A hit is an untrusted hint, not authority. Reuse requires all of: a
well-formed index address; store bytes whose Blake3 digest equals it; a
decodable wrapper; a bundled claim exactly equal to the precomputed slot
CheckEnv; a safely decoded Aiur proof (Aiur.Proof.ofBytesChecked — the
former trusted-byte constructor panicked on truncated bincode, and malformed
cache bytes must never abort the command); and native verification under the
exact precomputed outer claim and local recursion system. Any failure logs a
miss and re-proves. Cache write failures are warnings. On a valid hit the
host reconstructs the slot's preimages from the manifest statement, so
parents need no trusted metadata from the cache. --no-cache bypasses index
reads and intermediate writes while still persisting the final root wrapper.

RAM-gated parallel scheduling (WP-B)

The post-order fold executes as a dependency DAG: leaves are ready
immediately; a join becomes ready when both child slots have completed. The
controller sorts ready work by descending RAM weight (slot number breaks
ties) and starts one dedicated IO.asTask per admitted slot around the
existing proof FFI. Two independent limits govern admission:

  • --jobs N caps active slots; absent or 0 means every ready slot may run
    if it fits the budget;
  • --max-ram G caps total reserved GiB in flight, defaulting to 92% of Linux
    MemTotal.

As in the existing Rust RamGate, a slot whose estimate exceeds the entire
budget is admitted only while nothing else runs — a conservative estimate
serializes work rather than deadlocking. The calibration-pending per-shape
weights are:

raw IxVM leaf = 4 GiB
shape 0/1 wrap = 195 GiB
shape 2/6 IxVM + IxVM = 390 GiB
shape 3/4/7/8 mixed pair = 340 GiB
shape 5 self + self flat = 195 GiB + 1 MiB × subject leaves
shape 9 self + self struct = 195 GiB

The 195 GiB wrap reserve carries forward the recorded q=100 lift upper bound —
the measured peaks above (186.8–195.8 GiB) bracket it exactly. The 390/340 GiB
direct and mixed estimates deliberately keep the non-default policy out of the
256 GiB fleet tier pending M1-f calibration; measured self-pair peaks
(102.5–156.9 GiB) make their weights conservative in the safe direction. Real
calibration on a known-core box will tighten them without changing scheduler
semantics.

Results are installed by slot index, never arrival order; each worker gets an
immutable snapshot of its completed children, so parallel timing cannot
change parent advice. On failure the controller stops admitting, drains every
running proof, and reports the lowest failed slot; successful independent
tasks may finish publishing valid cache entries. Concurrent proofs share
Aiur's rayon global pool, so throughput can sub-scale even when memory
permits multiple slots; process-per-slot execution is a measurement-driven
follow-up.

Converged activation audit (WP-D + M1-e)

lake exe IxTests aggregate-activation is a dedicated diagnostic kept out of
the default suite. It executes the generated production ix_aggr path across
4 wraps and 128 pairs: two controlled child trace heights, shapes 0–9,
optional assumptions independently per side, complete discharge versus
explicit carry, and all four child-kind combinations in both flat and
structural modes. Every execution returns per-circuit (uniqueRows, totalHits); the runner performs the full 132-case matrix twice and rejects any
difference before rendering a deterministic per-shape Markdown report.
Current signature:

263a3087d42340613b012d50a9416e23549f85aa1ea425e86bf0a216c7b3e27d

Result: 183 audited circuits — 154 active in every case, 29 input-dependent,
zero never observed. The variable set concentrates in the expected paths:
wrap dispatch, canonical set folding, structural Merkle discharge, and
child-kind decoding. Fixed-height Bytes1/Bytes2 circuits are
outside the execute FFI's query-count array and are called out explicitly.
The audit records information only; dummy calls would add cost for the
currently planned SP1 terminal and should be chosen only if a future static
terminal requires input-independent activation.

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.executeMultiStarkJoin for generated or interpreted flat
    and structural join execution;
  • AiurSystem.proveMultiStarkJoin for witness generation and proving;
  • JoinAdvice, JoinPreimage, JoinTree, JoinPath, strict framed decoders,
    and the seven-channel join_io_buffer;
  • the matching rs_aiur_multi_stark_join_execute and
    rs_aiur_multi_stark_join_prove externs; and
  • Aiur.Proof.ofBytesChecked for panic-free store-boundary proof decoding.

The retained three-entrypoint executor is parity-checked against the bytecode
interpreter on both output and every circuit's unique-row/total-hit counts. The
combined verifier contains 247 Aiur functions; the regenerated Rust file is
2,239,894 bytes (ixvm: 5,807,375 bytes / 780 functions). Most line churn is
generated code. The protocol source of truth is
Ix/MultiStark/Aggregate.lean; the native advice contract lives in
aiur_multi_stark_runner.rs. It is now an Ix-agnostic regression/reference backend,
not the production CLI backend.

The production ix_aggr executor is independently parity-checked for wrap,
flat, and structural shapes. It contains 248 Aiur functions and its generated
Rust file is 2,061,361 bytes. Its source of truth is
Ix/Aggr/Circuit.lean, with native advice construction in
aiur_ix_aggr_runner.rs; all three generated artifacts are checked with
ix codegen --check.

Benchmarks

M1-f replaces the legacy two-shard/three-entrypoint runner with
bench-aggregate-policy, a production ix_aggr benchmark for one exact
four-shard fixture. It selects the four retained leaves from a validated and
pruned manifest, contracts the surrounding tree without changing orientation,
and independently reconstructs the expected host root. Wrap-first uses shape 0
leaves; direct mode keeps IxVM leaves raw. Both policies then select their flat
or structural heterogeneous shapes from the production plan. Measured runs
require q=100, jobs=1, --no-cache, and JSON output; the harness natively
verifies all four inputs and every recursive output, persists every completed
slot as an ordinary Ixon.Proof, and writes resumable per-slot and whole-run
timing/RSS/proof metadata.

The existing bench-typecheck --recursive --join row keeps its stable
join-* reporting/dashboard schema, but its join phase now exercises direct
ix_aggr shape 2 instead of the retired join_two circuit. Historical size
baselines remain recorded: 7,986,166 bytes (7,986,204 with the Ixon.Proof
wrapper) for a one-constant q=100 lift under the pre-E2 247-function system,
versus 7,443,023 bytes (7,443,061-byte wrapper) after WP-E2, a 543,143-byte /
6.80% reduction. Pruned multiproof length depends on sampled query-path
overlap, so a positive-PoW output is now a sample rather than an exact
byte-length pin. The deterministic q=100/PoW-0 regression signal is 7,447,279
bytes and 38,850,942,825 recursive-verifier FFT cost. The target-box policy
comparison is the remaining measurement, not remaining harness work.

Test coverage

The legacy aggregate-first suite uses a small stand-in child system so it can
create real Multi-STARK child proofs without the tens to hundreds of GiB
required by a full recursion proof. Those proofs are consumed by the retained
join circuits, so proof validation, statement decoding, set/path folding,
native advice, and transitive recursion are exercised end-to-end at execution
time.

The 57 passing legacy aggregate-first cases cover:

  • canonical flat and structural host folds; manifest parsing, validation, and
    post-order lowering;
  • distinct lift/flat/structural compiled entrypoint identities;
  • honest flat union and cross-child discharge; structural path discharge plus
    a carried assumption;
  • flat-join and structural-join outer claim layout and native verification;
  • transitively pinned flat and structural children; rejection of a child
    carrying a different allowed digest;
  • generated/interpreter parity for both join modes; threshold scheduling that
    is flat below and structural above monotonically;
  • strict keyed-blob framing; wrong-root and tampered-sibling paths; a missing
    path choice; a carried candidate omitted from the output; the obsolete
    88-byte allowed blob; a flat join fed a genuinely free-form structural
    child root; omitted/extra assumptions or subjects; noncanonical unsorted
    trees; tampered child proofs;
  • a synthetic three-leaf manifest with empty outer leaves: raw coverage before
    pruning, contraction, dense remapping, and singleton-lift classification;
  • claim-only shard preparation preserving exact subject/assumption trees,
    with the one-pass witness closure matching the legacy per-owned-root union;
  • exact legacy verifier reconstruction of a lift outer claim and native
    verification under it;
  • byte-for-byte equality of the shared default recursion builder with the
    former direct construction; the exact 40-byte FRI cache encoding;
    independent recursion-vk identity changes for FRI and commitment overrides;
  • cache-key invalidation across version, recursion vk, FRI parameters, and
    outer claim; whole-plan statement/claim/key derivation before proving;
    atomic index creation, corrupt-index detection and repair; a
    content-addressed, claim-bound, natively verified resume hit; mismatched
    claim rejection; corrupt store-content fallback; checked rejection of
    malformed proof bytes without a Rust panic;
  • heaviest-first admission with deterministic tie-breaking; job and byte
    ceilings; dependency release; admit-alone oversize handling; the flat-join
    affine weight and MemTotal parser; jobs=1/jobs=2 payload equality;
    failure draining without admitting a dependent join; jobs=1/jobs=2
    byte-identical recursive wrappers under canonical zero-PoW grinding, with
    each scheduled child decoded and verified; and
  • Rust writer normalization plus round-tripping a genuinely noncanonical
    structural CheckEnv root.

The wrapper-byte scheduler gate uses zero PoW deliberately: with the pinned
positive-PoW implementation, rayon's find_any selects any passing grind
witness, so two valid serial proofs already differ in bytes. Production
jobs=1/jobs=2 correctness is the stronger protocol invariant — exact claims
and native verification under the same recursion system. The primitive
Multi-STARK and recursive-verifier suites were rerun to guard the shared
verifier and lift paths, and the separate activation gate covers 132 accepted
shape/input cases twice.

The focused ix-aggr command now runs 93 passing checks. Its 42
circuit/shape/plan checks cover both wraps, all flat and structural child-kind
pairs, a nested structural self child, generated/interpreter parity, strict
tree and native keyed-blob framing, identity/shape closure, wrong or tampered
paths, omitted carries, unsorted trees, flat↔structural hint confusion, a flat
parent fed a free-form structural root, and exact four-shard wrap-first/direct
plans. Its 51 converged semantic checks cover uniform claims, cache v2 key
invalidation and verified resume, recursion parameters, manifest
validation/pruning, singleton/canonical/hybrid value reconstruction,
shard-preparation closure sharing, wrap-first/direct planning, per-shape RAM
admission, dependency/failure behavior, and jobs=1/jobs=2 determinism including
concurrent zero-PoW proving. Rust unit tests separately pin channel-6 key layout
and strict path-blob framing.

Validation run

All of the following pass on the current stack:

nix develop --command lake exe IxTests aggregate-first # 57/57nix develop --command lake exe IxTests ix-aggr # 93/93nix develop --command lake exe IxTests aggregate-activation # 132 cases × 2 stable passesnix develop --command lake exe IxTests multi-stark # 12/12nix develop --command lake exe IxTests recursive-verifier # 6/6nix develop --command lake build IxTests bench-typecheck bench-recursion-debug bench-aggregate-policy ixnix develop --command cargo test -p ixvm-codegen aiur_multi_stark_runnernix develop --command cargo test -p ixvm-codegen aiur_ix_aggr_runnernix develop --command cargo test -p ix-kernel shard::tests # 23/23nix develop --command cargo test -p ixon test_check_env_claim_accepts_structural_rootnix develop --command cargo clippy -p ixvm-codegen -p ix-ffi -p ix-kernel -p ixon --tests -- -D warningsnix develop --command cargo fmt --all -- --checknix develop --command lake exe ix codegen --checkgit diff --check

Both benchmark entrypoints also reject --queries 0 with a usage error before
building or proving, and a stored pre-E2 proof now reports a checked decode
error rather than aborting. The fresh post-E2 aggregate fixture verifies
against both its .ixe and .ixes inputs.

A q=1 live smoke of the converged typecheck join also executed, proved, and
verified direct shape 2 successfully: 1.221742 s execution, 26,738,665,748 FFT
cost, 52.380108 s proving, 21,565,714,432-byte peak RSS, 401,340-byte proof,
and 0.002616 s verification. The M1-f policy harness passed plan-only smoke for
both policies and emitted the expected resumable JSON schema.

Pre-convergence real-environment gates on the 124 GB development host (kept
as WP-A/WP-B baselines for the M1-f rerun):

  • A two-shard manifest from the 5,986-constant tc-parity.ixe environment
    passes coverage and structural scheduling with threshold zero
    (2 lifts + 1 binary joins (1 structural)), and its one-shard variant
    plans 1 lifts + 0 binary joins.
  • Before E2, a one-constant q=100 production lift ran end-to-end through the
    cache and scheduler, establishing WP-A's invalidation/repair baseline:
PassResultWall
Initialproved and cached root 090bea6f…ca1535~64 s
Identical rerunverified cache hit, same root1.66 s
Truncated indexclean miss, re-proof, atomic repair, same root~38 s
Post-repair rerunverified cache hit, same root1.79 s
--jobs 2 --max-ram 400195 GiB reserved, verified hit, same root1.45 s

At scale, the measured pre-E2 4-shard Init run in Measured performance
above exercised the former backend with real shard proofs, four q=100 lifts,
two lower joins, and a root join with genuine assumption discharge. The pinned
M1-f harness will repeat this fixture under both converged policies.

Soundness summary

  • Every present child is fully verified under the verifying key selected by
    its shape-constrained kind.
  • Child function indices are constrained to IxVM verify_claim or ix_aggr,
    and every recursive child must carry the same allowed digest, pinning the
    two verifying keys and two entrypoints transitively.
  • Claims and allowed data are Blake3-bound and strictly parsed with no trailing
    bytes.
  • Flat mode reopens canonical roots and proves exact union/difference;
    structural mode proves the exact root-of-roots relation and accounts for
    every unique input assumption through either inclusion or explicit carry.
  • A bogus path, dropped candidate, extra survivor, malformed choice, or
    noncanonical assumption tree rejects. Over-carrying and duplicate subject
    leaves can only weaken the output claim; the CLI requires an unconditional
    final root.
  • Coverage is checked on the unmodified manifest, so pruning cannot conceal a
    hole or overlap; only leaves with an environment-derived owned-constant
    count of zero are removed, and contraction preserves the order and shape of
    retained leaves.
  • A singleton root is a verified shape-0 wrapper, never a raw-proof shortcut;
    the circuit binds its output digest directly to the pinned IxVM child's
    CheckEnv digest.
  • Manifest-aware verification independently repeats pruning, scheduling, and
    value folding, then verifies the one derived ix_aggr outer claim. No
    claimed proof kind or shape is accepted from wrapper metadata.
  • Proving and verification build the recursion system from the same explicit
    parameter object; any parameter change necessarily changes the recursion vk,
    allowed-system identity, and cache key.
  • Cache keys bind version, recursion-vk digest, explicit FRI bytes, and exact
    outer claim, all derived before proving; hits re-hash store content, bind
    the claim, decode without panicking, and verify natively. Corrupt, missing,
    or stale entries are misses, never trusted successes or command failures.
  • A pair is admitted only after both child slots complete; immutable
    snapshots and indexed installation make arrival order irrelevant. The
    controller owns all reservations; failures drain running work before exit.
  • Free-form structural roots are serialized as opaque commitments
    (crates/kernel/src/claim.rs); canonical openings are required only by
    protocol paths that explicitly need them.

Review map

Commit-by-commit review is recommended — each commit is feature-scoped with
its own tests (map in the first comment / commit list).

AreaPrimary filesWhat to review
Legacy regression protocolIx/MultiStark/Aggregate.lean, Ix/MultiStark.leanpre-convergence 96-byte identity and semantic/audit reference surface
Production circuitIx/Aggr/Circuit.lean, Ix/Aggr.lean80-byte identity, uniform claims, shapes 0–9, heterogeneous child pinning, structural path checks
Converged host modelIx/Aggr/Host.leanfree-form structural roots, canonical survivors, path extraction
Host statement modelIx/MultiStark/Host.leanfree-form subject roots, canonical assumptions, path extraction
Manifest parsing + pruningIx/Cli/CheckCmd.leanstrict tree parsing, validate-before-prune, unary contraction, dense remap
Aggregate driverIx/Cli/AggregateCmd.leanwrap-first/direct shape policy, singleton wrap, uniform slot claims, cache v2, per-shape RAM weights, verified DAG resume/failure draining
Root verificationIx/Cli/VerifyCmd.leanone ix_aggr outer claim, value-based environment comparison, manifest-relative hybrid reconstruction
Recursion parametersIx/MultiStark.lean, Ix/Aggr.lean, Ix/Cli/{Aggregate,Verify}Cmd.leanshared config plumbing, deterministic systems, stable FRI cache encoding
Pruned FRI multiproofsIx/Aiur/{Protocol,Semantics/BytecodeFfi}.lean, Ix/Cli/{Aggregate,Verify}Cmd.lean, crates/ffi/src/aiur/protocol.rscompact persisted proof vs expanded recursive advice boundary, checked legacy-proof failure
Cache/store boundaryIx/Store.lean, Ix/Aiur/Protocol.lean, crates/ffi/src/aiur/protocol.rswipeable namespace, checked proof decoding, corrupt-data fallthrough
Shard preparationIx/IxVM/ClaimHarness.lean, Ix/Cli/{Aggregate,Check,Verify}Cmd.leanclaim/tree-only consumers, one-pass multi-root witness closure
Native advicecrates/ixvm-codegen/src/aiur_multi_stark_runner.rsstrict framing and seven-channel key layout
Converged native advicecrates/ixvm-codegen/src/aiur_ix_aggr_runner.rs, crates/ffi/src/aiur/protocol.rsstrict path framing, shared channel-6 key layout, execute/prove passthrough
Generated executorcrates/ixvm-codegen/src/aiur_multi_stark.rsgenerated artifact; verify with ix codegen --check
Converged generated executorcrates/ixvm-codegen/src/aiur_ix_aggr.rsgenerated artifact; verify with ix codegen --check
Manifest writercrates/kernel/src/shard.rsproducer-side omission, tree remap, aggregate metadata
Claim semanticscrates/kernel/src/claim.rs, crates/ixon/src/proof.rsfree-form root contract and codec regression test
TestsTests/Aggr.lean, Tests/AggrSemantics.lean, Tests/AggrActivation.lean, Tests/MultiStark.lean93 production shape/semantic/plan checks, 132-case activation audit, retained Ix-agnostic regressions
BenchmarksBenchmarks/AggregatePolicy.lean, Benchmarks/Typecheck.lean, bench registry/dashboardspinned four-shard wrap-first/direct handoff, resumable JSON, converged direct-shape-2 join metrics, historical lift-size pins

Current limits and non-goals

  • Recursion FRI parameters stay at the q=100 defaults; the q=50 + compensating
    PoW policy is an explicit follow-up decision (the measured costs above are
    therefore the conservative end).
  • Structural roots are manifest-relative. Environment-only verification
    accepts only a bundled value equal to the canonical environment root;
    hybrid roots should be verified with both --ixe and --ixes and the
    proving threshold.
  • Scheduler RAM weights are calibration-pending placeholders (validated in
    the safe direction by the measured run); concurrent proves share the rayon
    global pool, so throughput can sub-scale — process-per-slot is a
    measurement-driven follow-up.
  • Same-key cross-process cache writes degrade to a warning and recomputation,
    never a soundness issue.
  • Dummy-call activation padding is deferred until/unless a static terminal
    circuit is selected; under the SP1 terminal it is pure cost.
  • Aggregate wrappers reuse Ixon.Proof and do not encode the proof system;
    callers must pass ix verify --aggregate.
  • M1-a–f land the converged production circuit, host/native substrate, driver,
    cache, scheduler, value-based verifier, semantic union, widened activation
    audit, and policy benchmark harness. The only remaining M1 work is executing
    the wrap-first/direct comparison on the target box. The pinned M1-f revision,
    inherited M1-e preflight, and both measured policies are one checkout/session;
    its handoff is documented in
    plans/aggregate-first-m1f-large-box-runbook.md.
  • This PR does not adopt cold-circuit grouping and does not implement a
    terminal KZG/SP1 wrapper.

Follow-ups

  1. Calibrate lift/join unit costs on a known-core box, with per-node
    flat-vs-structural attribution, and measure frontier/discharge-path counts
    at Mathlib scale (plan §11.4.1).
  2. Decide the recursion q=50 + PoW policy (§9.5) now that the mechanism
    exists.
  3. Connect the root proof to the selected terminal compression path (E3:
    single-recursion-proof SP1 guest + ix compress-root).
  4. Re-evaluate cold-circuit grouping (E4); E2's measured outer-lift reduction
    was only 6.80%, so grouping may still be material.
  5. Process-per-slot proving if measured rayon contention warrants it.
  6. Run the exact M1-f commit
    b70042d6d1a029bb8e1d541da7aef5e0613d67e6 for the combined M1-e preflight
    and wrap-first/direct policy benchmark session in the large-box handoff
    runbook.

johnchandlerburnhamand others added 22 commits August 27, 2026 16:21
Add the deterministic lift/flat/structural activation matrix and keep dummy calls deferred. Split aggregate recursion commitment/FRI configuration from IxVM defaults, share it between proving and verification, and pin the future cache encoding without changing active protocol parameters.
Precompute versioned per-slot cache keys, persist lift and join wrappers in the content-addressed store, and reuse entries only after exact claim and native outer-proof verification. Add safe proof decoding, corruption recovery, and --no-cache.
Execute ready lift and join slots as a dependency DAG under explicit job and RAM admission. Add calibration-pending slot weights, failure draining, CLI controls, and serial/parallel scheduler and proof-equivalence gates.
Add an opt-in two-child benchmark that proves singleton CheckEnv shards, lifts both proofs, and measures a verified flat join. Wire join metrics through reporting and dashboards, and record the current pre-E2 lift-size baseline.
- rust-toolchain.toml channel: 1.92 → 1.98, with the matching fenix
toolchain hash in flake.nix. The pinned fenix already carries the 1.98
release manifest, so flake.lock needs no change (fenix's nixpkgs stays
pinned via its lean4-nix follows, so the Lean toolchain is untouched).
- Drop clippy::from_iter_instead_of_collect from the workspace lints:
removed in clippy 1.98 and now warns as unknown.
- Fix the warnings new clippy 1.98 lints surface across the workspace:
chunks_exact(N) → as_chunks::<N>() where the chunk size is constant,
descending sort_by → sort_by_key(Reverse(..)), iteration over map
values via .values(), map().unwrap_or() → map_or(), a checked
division, an unwrap-after-is_some restructured into if-let, and
assorted redundant-reference/pattern cleanups (mostly cargo clippy
--fix). The two byte-gadget files keep their chunks_exact warnings
until the next commit, which rewrites those regions anyway.
CI derives its Rust version from rust-toolchain.toml, so no workflow
changes are needed.
Companion to multi-stark's update-p3 branch (c72d321 → 249b740), which
carries four soundness/robustness fixes and the Plonky3 v0.6.0 bump
(pruned FRI Merkle multiproofs: ~2x faster verification, 40-70% smaller
proofs; canonical Goldilocks serde removes proof-byte malleability).
Proofs and verifying keys are not compatible with the previous pin.
Integration:
- Lookup gained max_multiplicity, a declared per-row bound on the
multiplicity's integer magnitude feeding the newly enforced logUp
height bound Σ wᵢ·hᵢ + |claims| < p. Function-circuit slots accumulate
mutually-exclusive branch selectors, so they declare 1; committed
count columns (function return slots, the memory circuit, the byte
gadget tables) declare the new COUNT_COLUMN_BUDGET (2^32 queries per
entry).
- The VK wire format carries the bound: u64 LE max_multiplicity per
lookup, between the multiplicity node id and the arg count. The
in-circuit VK deserializer (Ix/MultiStark/SystemDeserialize.lean)
parses past it; the value is bound through the vk digest but the
height bound itself is not yet enforced in-circuit (the native
verifier enforces it).
- Message fingerprints are width-bound by default upstream (the slot
width seeds the Horner fold), which is incompatible with aiur's
branch-shared lookup slots: mutually exclusive branches superpose
messages of different natural widths into one slot at the maximum
width, so a narrow call is sent zero-padded to a width its callee's
return slot never provides, and proving fails with
UnbalancedChannel (pinned as the prove_verify_mismatched_call_widths
regression). Aiur instead declares WidthBinding::ByConstruction —
the plain Horner fold, restoring zero-padding transparency — and
takes on the prefix-freeness contract that makes it sound: every
message's natural width is a function of its constant-constrained
leading prefix (channel tag plus discriminator: fun_idx fixes
2+in+out, the memory size coordinate fixes 3+size, each gadget tag
fixes its table width), so zero-extension can only equate a padded
message with its own natural form. The contract is documented at the
channel constants in lib.rs; the declaration is applied in
AiurSystem::build and mirrored in the vk_codec decoder so decoded
VKs replay the same transcript. ByConstruction is also exactly the
fold the in-circuit verifier's logup_fingerprint already computes,
so the recursive verifier needs no fingerprint change.
- The policy is Fiat-Shamir-bound as the first observe_shape word; the
in-circuit transcript replay prepends the matching limb.
- aiur_multi_stark.rs regenerated (ix codegen) for the deserializer
and transcript changes.
- P3 v0.6.0 ships FRI query openings as pruned Merkle multiproofs,
while the in-circuit verifier consumes one authentication path per
query (its per-query control flow is a far smaller circuit than the
amortized multiproof walk). Rather than porting the walk into the
DSL, the proof advice stays in the per-query transport: multi-stark's
new advice module re-encodes a natively-verified proof by running
p3's own verification with a recording compression function and
reading each query's path back out of the recorded digest map. The
advice bytes are untrusted verifier input, never digest-bound —
the transcript binds the commitments and every expanded sibling is
authenticated against them per query — so pruning vs expansion is
pure transport and the encoding choice is sound. AiurSystem gains
proof_to_advice_bytes (FFI: AiurSystem.proofToAdviceBytes); the
recursive-verifier test feeds it instead of Proof.toBytes, whose
native wire format is still round-tripped separately. The Lean-side
proof grammar and the codegen'd verifier are byte-identical to
before — no in-circuit changes.
Claim layout, the VK wire format above and aiur's public semantics are
otherwise unchanged; the policy adds no prover or verifier work over
the previous pin.
Still open, native-verifier-only: the logUp height bound is parsed
past but not yet enforced in-circuit (a wide-arithmetic check, tracked
separately).
The Lean v4.33.1 update pinned both dependencies at revisions that
predate their Rust 1.98 bumps; their heads now carry those bumps, which
this workspace needs since rust-toolchain.toml moved to 1.98. Both
revisions stay on leanprover/lean4:v4.33.1.
- Blake3.lean 1b0fbd2 → e6e908b (Rust 1.98, plus a case-insensitive
source-directory fix), updated in lakefile.lean, lake-manifest.json
and the blake3-lean flake input. The revision keeps the
`blake3_rs_shared` target the `ix_native_decide_dynlib` pin requires.
The inherited entry in Benchmarks/Compile/lake-manifest.json was still
on the pre-4.33.1 revision and now tracks the root pin.
- lean-ffi 2a9c91e → 93c7e52 (Rust 1.98). Only bignat reaches the sp1
and zisk workspaces, so their lock files move that one package.
The !benchmark recursive phase reported n/a for every fri-verifier
metric: Benchmarks/Typecheck.lean still fed Proof.toBytes — the pruned
multiproof wire format — to executeMultiStark/proveMultiStark, so the
in-circuit verifier rejected on parse and the harness (correctly) left
the recursive fields absent rather than emit a fake datum. The
in-circuit verifier consumes the per-query advice transport
(AiurSystem.proofToAdviceBytes); proofBytes stays the reported
proof-size metric.
bench-recursion-debug had the same advice-format gap plus a stale
claim recipe: it still built the public input as 32 raw digest bytes,
predating the ClaimHarness.packedDigestKey packing bench-typecheck
uses (its own out-of-circuit sanity check failed with
InvalidPowWitness — a wrong claim diverges every challenge — and the
advice re-encoder refused the proof for the same reason). Both aligned
with the typecheck flow.
Validated end-to-end at production parameters (numQueries 100,
query PoW 20, blowup 2) on Nat.add_comm: inner prove, advice
re-encoding, and the codegen'd in-circuit verifier accepting.
Keep compact proof bytes at storage and cache boundaries, expand them only for recursive lift/join advice, reject unsupported zero-query benchmarks, and decode legacy store proofs without panicking. Refresh tests, benchmark pins, and Rust 1.98 lint compatibility.
Add Ix/Aggr, a recursive aggregation system for IxVM shard proofs that
keeps Ix/MultiStark untouched and Ix-agnostic. One entrypoint, ix_aggr,
subsumes lifting and joining: a one-byte advice shape selects wrap or
binary join over any mix of IxVM and ix_aggr children, so shard proofs
enter the recursion system directly as join children and the dedicated
lift stage disappears.
Circuit (Ix/Aggr/Circuit.lean): every shape verifies its children in
full (verify + ood_verify from the shared Multi-STARK verifier modules)
against the vk its hinted kind demands. Identity is one 80-byte
digest-bound blob - blake3(ixvm vk) || verify_claim idx ||
blake3(self vk) || ix_aggr idx - carried unchanged at every node; self
children must bind the identical blob digest, pinning both vks and both
entrypoint indices transitively. Claims are a uniform 18-word
[0, aggr_idx, allowed(8), checkEnv(8)] at every depth, so proofs of
different tree levels combine freely. Wrap shapes bind the output
digest to the child CheckEnv digest directly; pair shapes open both
CheckEnv preimages, re-root the canonical subject/assumption trees, and
prove subjects = L ∪ R, assumptions = (asmL ∪ asmR) ∖ subjects with
linear sorted merges.
Toplevel (Ix/Aggr.lean): ixAggr = MultiStark.multiStarkFull + circuit,
pruned to ix_aggr, so verify_multi_stark_proof and other unrelated
entries no longer pad aggregate proofs. The host half of the wire
contracts (allowed blob, public input packing, shape codes, keyed
preimage/tree blob framing, interpreter IO assembly) lives beside the
toplevel; Ix/Aggr/Host.lean folds CheckEnvTrees statements.
Native path: ix codegen gains the ix-aggr target
(crates/ixvm-codegen/src/aiur_ix_aggr.rs, 244 fns); its runner builds
the seven-channel IO buffer natively and routes execution through the
generated code. New FFI rs_aiur_ix_aggr_execute/_prove
(executeIxAggr/proveIxAggr) pass proofs, vks, claims, and the compact
count/key/length preimage/tree blobs without per-byte boxing, plus
Proof.ofBytesChecked for store-boundary decoding.
CLI: ix aggr --ixe E --ixes M <shard-proof>... reconstructs every
nonempty shard statement from the env, matches wrappers by claim
digest, natively pre-verifies them, folds a balanced bisection (the
canonical fold makes the root claim independent of tree shape), wraps
single-shard roots so the persisted root is always an ix_aggr proof,
checks the root closes over the env canonical tree with no residual
assumptions, and persists the wrapper.
Tests (lake test -- ix-aggr, 17 cases): all five shapes accept over
real Multi-STARK stand-in child proofs from two distinct-vk systems;
codegen'd execution matches the interpreter on output and per-circuit
query counts for wrap and pair; negatives break one binding each -
lying shape hint, tampered proof, foreign identity, wrap statement
drift, dropped assumption, padded subject set, and tree advice not
reproducing its keyed root.
Replace the legacy lift/join entrypoints with the heterogeneous ix_aggr system across aggregate, verification, cache, and codegen paths. Preserve wrap-first and direct-join policies behind one proof identity and cache namespace.
Add the converged 91-check semantic suite and a deterministic 132-case activation audit covering shapes 0 through 9 twice, with no unobserved circuits.
Replace the legacy two-shard three-entrypoint benchmark with a four-shard manifest-subtree harness for production ix_aggr wrap-first and direct policies. Enforce the q=100 serialized no-cache profile, natively verify every input and recursive output, persist ordinary aggregate wrappers, and emit resumable per-slot JSON metrics.
Port bench-typecheck --join to ix_aggr direct shape 2 while preserving the stable join metric schema. Remove the final legacy slot-spec/preimage shim and pin both M1-f four-shard plans in the focused suite.
Update the multi-stark dependency and Rust toolchain for Plonky3 0.6, along with the Rust 1.98 lint migrations required to keep the workspace warning-free. Refresh the Rust-compatible Blake3.lean pin in both root and compile-package manifests.
Adapt recursive Aiur verification to Plonky3's pruned FRI multiproofs. Native proofs retain their compact serialized representation and native verification path; the FFI expands authenticated Merkle frontiers into per-query advice only when entering the existing recursive verifier circuit.
Preserve the packed claim-digest convention in the recursion diagnostic and exercise the advice boundary in the end-to-end test and benchmark paths. CPU and CUDA recursive q1 runs produce identical 823,485-byte inner proofs and 331,273-byte outer proofs.
The q50 Vector.extract_append workload retains identical CPU/CUDA proof sizes. Inner plus outer STARK proving measures 65.87s on CPU and 8.81s with CUDA on the RTX PRO 6000, a 7.48x speedup.
PR benchmark runs execute trusted workflow YAML from the default branch while loading composite actions from the PR checkout. When Bencher data and binary caches are unavailable, the workflow checks out main under base/ and asks Lake to rebuild it without first installing the Rust channel pinned by that checkout.
Teach the existing CPU provenance action to install the base checkout's validated Rust channel and profile immediately before an uncached base build. The step is a no-op when the toolchain is already available and leaves cached benchmark comparisons unchanged.
Consume the Plonky3 0.6 batch-opening layout directly in Aiur instead of expanding every pruned Merkle frontier into one authentication path per FRI query. Sample all query indices from the unchanged transcript, sort and deduplicate them with an O(q log q) merge sort, authenticate each input and commit-phase commitment once, then retain the existing per-query reduced-opening and FRI arithmetic.
Bind every frontier to transcript-derived indices, consume boundary digests in Plonky3's level/parent/child order, reject trailing frontier elements and inconsistent duplicate leaves, and assert all native opening dimensions and sibling counts. Explicitly constrain the digest-bound protocol specialization to cap height 0, binary FRI, and a constant final polynomial. Move memo_u32_less_than into IxVM Core so both substitution and multiproof sorting share its constrained rows.
Strengthen the recursive negative test to mutate a structurally valid stage-1 commitment. Regenerate both checked-in Aiur Rust executors and retain interpreter/codegen query-count parity.
On Vector.extract_append q50, recursive-verifier FFT cost falls from 204.073B to 201.166B. CPU outer proving improves from 50.09s to 45.03s and the full CPU pipeline from 90.64s to 82.90s. GPU outer proving improves from 15.85s to 13.72s and the full GPU pipeline from 28.86s to 26.69s. The outer proof grows from 3.92 MB to 4.17 MB.
Validated with the MultiStark primitive suite, recursive honest/tamper/parity tests, codegen --check, release workspace clippy, release CUDA clippy, rustfmt, and diff checks.
@johnchandlerburnham

Copy link
Copy Markdown
MemberAuthor

Init full proving report

Revision: 82dcc9dba36d0cfd6c753c6878ab649643a52026
Host: 64 vCPU, 495 GiB RAM, 256 GiB swap
Final result: one verified ix_aggr FRI proof with no remaining assumptions.

Compilation and sharding

StepResult
Compile sourceBenchmarks/Compile/CompileInit.lean
Ix compiler time1.876s
Constants compiled65,994
init.ixe size194,296,853 bytes (185.3 MiB)
Static sharding time3.1s
Manifest16 nonempty shards
Retained/owned constants56,621
Manifest size2,619,404 bytes (2.50 MiB)
Predicted FFT per shard4.160e11–4.197e11
Predicted FFT spread1.01×

The 1.876-second compile measurement is the Ix compiler itself; it excludes the initial one-time dependency download and Lake build.

Stage 1: base shard proofs

All 16 shard proofs were persisted and individually verified against their corresponding manifest claims.

MetricSerial baselineTwo-way run used for aggregation
Concurrency12
End-to-end wall time24m 16.76s15m 53.93s
Sum of shard prover times22m 43.23s30m 08.14s
Per-shard time range74.08–113.97s98.94–142.29s
Median shard time82.66s108.75s
Highest individual RSS219.8 GiB219.8 GiB
Combined process-tree peak358.2 GiB
Proof count1616
Total proof-wrapper size369,995,038 bytes
Mean proof-wrapper size23,124,690 bytes
Proof-wrapper range22,714,520–23,998,692 bytes

Two-way concurrency gave a 1.53× wall-time speedup despite increasing total prover-seconds by about 33% from CPU/memory contention.

Stage 2: recursive aggregation

Policy:

  • Wrap-first
  • --structural-above 4096
  • --jobs 2
  • --max-ram 450
  • Default q=100 recursion parameters
  • Aggregate cache enabled

Plan:

OperationCount
Shard wraps16
Binary joins15
Structural joins14
Flat joins1
Total recursive slots31

Results:

MetricResult
Wall time45m 06.61s
Peak resident memory379.9 GiB
Average CPU utilization4,192% (~42 cores)
Cache hits0
Freshly proved/cached slots31
Swap used0 bytes
Exit status0
Final proof-wrapper size9,596,044 bytes (9.15 MiB)

Final proof

Root address:

80f6844f968920abaef8d8c750a1a1dc1ebe32657a3cfb824e1810bcd65c8814

Final claim:

CheckEnv(6dc8f821a989c6f7b9d520b053ef8edbd582d2408d7b6a6ff72b908666e03b6f, none)

Both validations passed:

  • Cryptographic-only aggregate verification
  • Manifest-aware verification against init.ixe, init-16.ixes, and structural threshold 4096

End-to-end measured time

Using the two-way Stage 1 run:

ComponentTime
Ix compilation1.876s
Sharding3.1s
Stage 1 proofs15m 53.932s
Stage 2 aggregation45m 06.61s
Measured core pipeline1h 01m 05.52s

This total excludes the one-time dependency build/download and a few seconds of manifest and final-root verification overhead. Using serial Stage 1 instead would make the measured core pipeline approximately 1h 09m 28.35s.

Use the PR77 lean-ffi and multi-stark pins, remove the stale width-binding and multiplicity-bound protocol fields, and regenerate the recursive and aggregate verifier sources.
Expose an opt-in x86_64-linux cuda shell with NVCC 13.2, sm_120 code generation, and safe host libcuda discovery while leaving the default development shell unchanged.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@johnchandlerburnham@samuelburnham@arthurpaulino
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root - #598

Open
johnchandlerburnham wants to merge 24 commits into
mainfrom
jcb/aggregate-first
Open

Aggregate-first recursion: lift shard proofs, fold CheckEnv claims, and verify one root#598
johnchandlerburnham wants to merge 24 commits into
mainfrom
jcb/aggregate-first

Conversation

@johnchandlerburnham

@johnchandlerburnhamjohnchandlerburnham commented Aug 27, 2026

Copy link
Copy Markdown
Member

Summary

This PR implements the aggregate-first path for Aiur Multi-STARK proofs. The
production path now runs through one ix_aggr recursion entrypoint: it wraps
independently-proven IxVM shards, folds their CheckEnv statements along the
manifest's bisection tree, and persists one recursive root proof with a
uniform 18-word outer claim. Every completed recursive slot is persisted in a
verified, content-addressed version-2 resume cache, and ready slots are proven
in parallel under explicit job and RAM limits.

The entrypoint has two pair-fold modes:

  • Flat shapes perform a canonical set union/difference and are efficient for
    small lower nodes.
  • Structural shapes commit to nodeHash(leftRoot, rightRoot) in one hash and
    discharge assumptions with Merkle inclusion paths. Their 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 (default
4096 subject leaves): flat below the threshold, structural above it.

IxVM shard proofs
│
├── ix_aggr shape 0 (default wrap-first policy)
│ or remain raw with --direct-joins
▼
ix_aggr proofs carrying uniform CheckEnv statements
│
├── flat ix_aggr shapes 2–5 below the configured subject threshold
│ subjects := canonical(subjectsL ∪ subjectsR)
│ assumptions := (assumptionsL ∪ assumptionsR) ∖ subjects
│
└── structural ix_aggr shapes 6–9 above the threshold
subjects := nodeHash(subjectRootL, subjectRootR)
assumptions := candidates carried or discharged by inclusion path
▼
Ixon.Proof {
claim := CheckEnv(manifest-relative subject root, none),
proof := one recursive Multi-STARK root proof
}

A manifest with one retained (nonempty) shard is also a valid deliverable: its
root is one shape-0 wrapper, with no self-join padding (including under
--direct-joins). Zero-constant manifest leaves are pruned after raw coverage
validation:

raw manifest
|
| validate exact environment coverage before pruning
v
drop zero-constant leaves + contract unary tree nodes + remap retained ids
|
+-- one retained leaf --> wrap once and persist the ix_aggr root
|
`-- multiple leaves --> fold flat/structural joins as above

This is the stage-2 aggregation layer from
plans/aggregate-first-pipeline.md: shard proofs are aggregated before any
terminal KZG/SP1 compression, so a later wrapper only needs to consume one root
proof. Terminal compression remains outside this PR.

Section 14 implementation is complete through M1-f in this branch. The
ap/ix-aggr single-entrypoint base is reconciled with WP-E2's compact stored
proof / expanded recursive-advice boundary, and its one ix_aggr entrypoint
supports wraps, flat pairs, and structural pairs across both IxVM and recursive
children. ix aggregate now uses that backend for planning, proving, cache
resume, and final verification; ix verify --aggregate derives one uniform
outer claim and performs value-based environment/manifest checks. The old
ix aggr command and all three-entrypoint production wiring are retired. The
older Multi-STARK aggregate implementation remains as an Ix-agnostic regression
surface, not as a CLI backend. Production semantics, activation coverage, and
benchmarking now live entirely against ix_aggr. M1-f lands the pinned
four-shard bench-aggregate-policy handoff and ports the stable typecheck join
metric to direct shape 2; only execution of the policy comparison on the large
box remains. Operationally this is one future handoff: Arthur will check out
b70042d6d1a029bb8e1d541da7aef5e0613d67e6 once, run the inherited M1-e
correctness suite as untimed preflight, and then benchmark both policies. There
is no separate M1-e box run.

Measured performance (2026-08-29)

Arthur Paulino ran the pre-convergence pipeline end-to-end on four real Init
shard proofs (shards 8–11 of a 16-way partition, 12,493 constants; base shard
proving excluded; native verification of each output included; ~512 GB box).
These are the standing M1-f comparison baseline; the converged wrap-first and
direct policies have not yet been rerun on that box. The recursion used the
conservative q=100 / PoW 20 defaults this PR keeps:

StageTimePeak RAMOutput proof
Lift × 498.8–103.0 s (mean ≈ 101 s)186.8–195.8 GiB8,162,462 B each
Join 8+9, 10+1148.9 / 48.5 s~102.5 GiB9,502,843 B
Root join85.8 s156.9 GiB9,455,498 B
Total (serialized)10:23 wall195.8 GiB9,455,498 B

Notes: lift cost is shard-content-independent (four lifts within ±2%, tracking
the ~constant 20–23 MB shard-proof shape); the measured lift peak brackets the
scheduler's 195 GiB weight placeholder exactly; and an unserialized run of all
four lifts OOM'd at ~492 GiB — precisely the behavior the WP-B admission gate
prevents (on a 512 GB box its weights admit two lifts). Because shards 8–11
are a strict partition subset, the root carries a real frontier assumption
set, so the joins exercised genuine discharge work. Full analysis, including
the comparison against direct (unlifted) IxVM joins, is in
plans/aggregate-first-pipeline.md §3.4 and the Zulip thread.

What lands

Protocol / circuit layer:

  • One production recursion entrypoint, ix_aggr, under one verifying key.
    Shapes 0–1 wrap one IxVM/recursive child, shapes 2–5 fold flat pairs, and
    shapes 6–9 fold structural pairs for every child-kind combination.
  • Recursive validation of wrap, flat-pair, and structural-pair children with
    transitive verifying-key, function-index, and allowed-system binding. A
    shape hint selects the verified form but cannot weaken its checks.
  • Strict in-circuit CheckEnv decoding and canonical flat set folding.
  • Structural subject folding with per-candidate Merkle discharge paths while
    keeping output assumption sets canonical and deduplicated.
  • One 80-byte protocol identity containing the IxVM/self verifying-key
    digests and their two accepted entrypoint indices; every recursive node has
    the same 18-word outer-claim layout regardless of shape.
  • Structural outputs bind nodeHash(leftRoot, rightRoot) and account for each
    unique assumption candidate through a strict Merkle-path-or-carry choice.
  • Host helpers construct structural roots, survivors, and path advice. Channel
    6 carries both the one-byte shape at key [0] and candidate path payloads at
    raw 32-byte address keys; the key shapes are disjoint.
  • Lean/Rust execute and prove FFI accept a compact keyed path blob, whose
    framing is decoded strictly before the circuit independently validates
    payload semantics.
  • The generated ix_aggr executor is regenerated at 2,061,361 bytes / 248
    Aiur functions and parity-tested against the interpreter.

Host driver and operations:

  • Strict .ixes aggregation-tree parsing and post-order lowering, with a
    balanced fallback for legacy manifests; validate-then-prune removal of
    zero-constant leaves with unary tree contraction (and a Rust writer that
    stops emitting them).
  • Single-shard roots: a one-retained-shard manifest always produces one
    shape-0 ix_aggr wrapper, including under --direct-joins.
  • A monotone leaf-count scheduler controlled by --structural-above, with a
    default wrap-first plan (shapes 0 then 5/9) and an explicit
    --direct-joins plan that derives heterogeneous shapes from actual child
    kinds.
  • Versioned, content-addressed aggregate cache and resume (WP-A): version 2
    binds the uniform outer claim, and reuse occurs only after content-digest,
    claim, decode, and native verification all pass; --no-cache bypasses.
  • Parallel execution of ready wraps/pairs as a dependency DAG under --jobs
    and --max-ram (default 92% of MemTotal), heaviest-first, with failure
    draining (WP-B). Admission weights are shape-aware: 4 GiB for a retained
    raw IxVM leaf, 195 GiB for wraps/self pairs, 390 GiB for IxVM/IxVM pairs,
    and 340 GiB for mixed pairs.
  • A recursion-parameter split (WP-E1): aggregate proving/verification share an
    explicit RecursionParameters value, defaulting to today's q=100 values;
    the q=50+PoW policy decision is deliberately deferred.
  • A deterministic 132-case production activation audit across shapes 0–9:
    two controlled child trace heights, both wraps, every flat/structural
    child-kind pair, optional assumptions per side, and discharge/carry. The
    complete matrix runs twice with a stable signature and observes every
    catalogued circuit; dummy-call padding remains intentionally deferred.
  • Claim-only shard preparation (no discarded dependency-byte closure walks)
    with a one-pass multi-root witness closure for proving.
  • ix aggregate --ixe E --ixes M [--structural-above N] [--direct-joins] [--jobs N] [--max-ram G] [--no-cache] [--plan-only] <proof>... and
    single-entrypoint aggregate verification with cryptographic-only,
    environment-bound, and manifest-bound value checks. The duplicate
    ix aggr command is removed.
  • Opt-in aggregate benchmarks: a tiny two-shard Init pair end-to-end bench and
    a flat-join bench (singleton CheckEnv shards → two lifts → verified flat
    join), with join metrics wired into reporting/dashboards, the pre-E2 lift
    proof size pinned at 7,986,166 bytes, and the post-E2 delta measured.
  • Positive, transitive-recursion, scheduler, cache, pruning, parity,
    malformed-input, and semantic-tampering tests: the aggregate-first suite
    has 58 cases, plus the separate activation matrix.

Protocol design

One recursion system, one entrypoint

Production builds Aggr.ixAggr, the shared Multi-STARK verifier closure pruned
to one ix_aggr entrypoint. A one-byte advice hint selects the exact verified
shape:

ShapeChildrenFold
0IxVMpass-through wrap
1ix_aggrpass-through wrap
2–5IxVM/ix_aggr pair (2 + 2·left + right)canonical flat fold
6–9IxVM/ix_aggr pair (6 + 2·left + right)structural fold

The hint is advice, not authority: each arm verifies the child proof(s) under
the key required by the encoded child kinds, then requires that kind's exact
claim layout. A wrong shape therefore fails proof verification, function-index
binding, or claim decoding.

Uniform public statement and pinned protocol identity

Every wrap and pair exposes the same public-input shape:

ix_aggr(allowed_digest: [G; 8], out_claim_digest: [G; 8])

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:

  1. the 80-byte allowed-system blob; and
  2. the serialized output CheckEnv claim.

The allowed blob is:

blake3(ixvm_vk)
|| verify_claim_idx:u64le
|| blake3(ix_aggr_vk)
|| ix_aggr_idx:u64le

The circuit requires exactly 80 bytes. 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
another function in those systems; binding the two accepted 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.
Every persisted aggregate outer claim is consequently the same 18-word form:
the ix_aggr function selector followed by the packed identity and output
claim digests. Shapes are witness choices and do not become root-kind metadata.

Child-proof validation and transitive pinning

Each pair reads the verifying key required by each child kind, checks its
Blake3 digest against the allowed blob, and deserializes it strictly. Wraps do
the same for their one child. For every child the selected arm:

  1. reads and fully consumes one proof and exactly one outer claim;
  2. runs the Multi-STARK structural verifier;
  3. runs ood_verify, including Fiat-Shamir replay, lookup accumulators,
    quotient/OOD checks, Merkle openings, and FRI verification; and
  4. decodes the statement according to the constrained child kind and function
    index.

The accepted forms are:

  • IxVM child: its system digest must equal blake3(ixvm_vk). Its claims
    digest opens to exactly one 10-word IxVM claim at the pinned
    verify_claim_idx, whose digest opens to a strict serialized CheckEnv
    claim.
  • ix_aggr child: its 18-word claim must use the pinned ix_aggr_idx; its
    first public digest must equal the current allowed_digest, transitively
    pinning both systems and both entrypoints; and its second digest opens to its
    output CheckEnv claim.

Any other index, shape, digest, trailing byte, or failed proof check rejects.
This keeps wrap→pair, flat→pair, and structural→structural composition on one
statement format.

Flat canonical folding

For child statements (S_L, A_L) and (S_R, A_R), flat shapes 2–5 enforce:

S_out = S_L ∪ S_R
A_out = (A_L ∪ A_R) ∖ S_out

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 u32 words using the
full-domain u32_less_than primitive. The implementation never uses Aiur
pointer 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

Structural shapes 6–9 perform the same allowed-blob binding, child-key
binding, two child-proof verifications, child decoding, and output-claim
binding as the flat shapes. They replace all subject-tree loading and set
re-rooting with:

S_out.root = nodeHash(S_L.root, S_R.root)

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:

  • carried (0): no bytes may follow, and the candidate must be the next
    value in the output assumption list; or
  • discharged (1): a bounded Merkle path from
    leafHash(candidate) to S_out.root must verify.

Path payloads are strict:

0
1 || count:u8 || repeated(side:u8 || sibling[32])

count is limited to 64. side = 0 hashes nodeHash(sibling, current) and
side = 1 hashes nodeHash(current, sibling); any other side rejects. The
payload 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

All ix_aggr shapes use the same seven-channel IO layout. Digest/root bindings
are checked before the corresponding bytes are decoded.

ChannelKeyValue
0[0], [1]first and optional second child proof advice bytes
1[kind]verifying-key bytes (0 = IxVM, 1 = ix_aggr)
2[0], [1], [2]left/right outer claims and output CheckEnv claim
3[0]80-byte allowed-system blob
4packed Blake3 digestnested claim preimages
5raw 32-byte addressserialized canonical trees keyed by root
6[0] / raw candidate addressshape byte / carried-or-discharged path choice

Lean passes channels 4–6 across FFI as compact framed blobs:

count:u32le || repeated(key[32] || payload_len:u32le || payload)

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.

Wraps receive no tree or path advice. Flat pairs receive full
subject/assumption tree advice and an empty path blob. Structural pairs receive
only the input/output assumption trees plus one path choice for every
deduplicated candidate; subject trees are never opened.

Manifest and host pipeline

.ixes aggregation tree

parseIxesManifest exposes both shard block lists and the optional binary
aggregation tree stored at the manifest tail. It:

  • requires nonzero, contiguous shard IDs;
  • validates option and tree tags;
  • requires tree leaves to be exactly the manifest shard set;
  • rejects duplicate/out-of-range leaves and trailing bytes; and
  • synthesizes a balanced ascending-ID tree for legacy manifests without a
    tree tail or with an explicit absent-tree tag.

The tree lowers to post-order FoldOp slots. Every join refers only to earlier
slots and the last slot is the root, providing a direct serial schedule and the
basis for cache keys and parallel execution.

Validate first, then prune empty manifest leaves

IxesManifestView retains the original shard id for every dense shard slot.
pruneEmpty:

  1. counts the environment constants owned by every raw shard;
  2. drops only shards with a count of zero;
  3. contracts aggregation-tree nodes with a removed child;
  4. densely remaps retained leaf indices; and
  5. returns the retained per-shard subject counts with the pruned view.

The aggregate and manifest-aware verifier paths deliberately call
shardsCover on the raw manifest before this transformation. Coverage
still establishes that every environment constant is owned exactly once. The
subsequent zero count is therefore evidence that a removed leaf contributes no
subject; pruning is not allowed to hide a missing or duplicated constant.

Original shard ids are preserved separately from dense scheduling ids, so CLI
plans and proof-binding errors continue to identify the source manifest shard.
An all-empty manifest is rejected because it cannot produce a root.

For a tree node node(left, right), pruning follows:

both retained -> node(left', right')
only left retained -> left'
only right retained -> right'
neither retained -> removed

The Rust .ixes writer (crates/kernel/src/shard.rs) also normalizes new
manifests before serialization — block-empty shard records are omitted, ids
rewritten densely, the tree pruned/contracted/remapped, and cross-ingress
recomputed. The Lean-side pass remains necessary for legacy manifests and for
the stronger environment-relative condition (a shard may contain blocks yet
own zero constants).

Monotone structural scheduling

The host counts actual owned constants for each shard after the disjoint-cover
check. schedulePlan annotates every slot with its cumulative subject count and
chooses a structural pair exactly when:

left.subjectCount + right.subjectCount > structuralAbove

The default threshold is 4096; --structural-above 0 makes every join
structural 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.

By default, every leaf is an ix_aggr shape-0 wrap, so binary slots are shape
5 below the threshold and shape 9 above it. With --direct-joins, a
multi-shard plan keeps leaves as raw IxVM children and derives each pair shape
from its actual child kinds: 2–5 for flat pairs or 6–9 for structural pairs.
The monotone threshold rule is unchanged.

The proving loop checks reconstructed subject counts against the schedule
before starting expensive work.

Singleton roots

ix aggregate imposes no two-shard minimum. The fold plan for one retained
leaf contains one shape-0 wrap and zero pairs; that wrapper and its CheckEnv
claim are persisted as the aggregate root. Even --direct-joins wraps a
singleton because the deliverable must verify under the one production
entrypoint. A self-pair would add work and manufacture a different statement.
Consequences:

  • exactly one proof is required per nonempty shard;
  • --plan-only reports one wrap and zero binary pairs for a singleton;
  • multi-shard flat/structural scheduling is unchanged; and
  • the final unconditional-root check always applies to an ix_aggr root.

ix aggregate

ix aggregate --ixe ENV.ixe --ixes SHARDS.ixes \ [--structural-above N] [--direct-joins] [--jobs N] [--max-ram G] [--no-cache] \ SHARD_PROOF_ADDR...

The command:

  1. deserializes the environment and manifest and verifies a disjoint exact
    shard cover on the raw manifest, then prunes zero-constant leaves;
  2. reconstructs every retained shard's exact CheckEnv claim and canonical
    trees (claim-only preparation — no dependency-byte closure);
  3. matches proof arguments to shards in any order by bundled claim digest,
    then requires exact claim equality and one proof per nonempty shard;
  4. natively verifies every IxVM shard proof before expensive recursion work;
  5. builds the IxVM and ix_aggr recursion systems and the pinned 80-byte
    allowed blob;
  6. derives every slot's statement, child kind, shape, uniform outer claim, and
    version-2 cache key up front;
  7. executes ready wraps and flat/structural pairs as a RAM-gated dependency
    DAG, resuming verified cached slots and persisting each completed wrapper
    (raw direct-policy leaves are verified inputs, not cache slots);
  8. canonicalizes all leaves under the final subject tree and checks that they
    reproduce the environment's canonical root;
  9. requires no remaining assumptions and natively verifies the outer proof at
    the one ix_aggr root entrypoint; and
  10. persists an Ixon.Proof containing the output CheckEnv claim and proof.

--plan-only stops after coverage validation and scheduling, resolving and
printing the scheduler policy without loading proofs or compiling recursion
systems:

ix aggregate --ixe ENV.ixe --ixes SHARDS.ixes \ --structural-above 0 --plan-only

ix verify --aggregate

Aggregate roots reuse Ixon.Proof, so verification is selected explicitly.
The verifier never accepts root-kind metadata. It serializes the bundled
CheckEnv value and reconstructs the only valid outer claim:

claim_bytes := serialize(CheckEnv(...))
outer_claim := ix_aggr(
blake3(allowed_blob),
blake3(claim_bytes))
InvocationCryptographic entrypointExpected statement value
ix verify --aggregate <proof>ix_aggrbundled claim; cryptographic proof only
ix verify --aggregate --ixe E <proof>ix_aggrcanonical environment root, unconditional
ix verify --aggregate --ixe E --ixes M <proof>ix_aggrpruned manifest fold, unconditional

Environment-only mode compares the bundled value to the canonical environment
root; a manifest-relative structural value therefore rejects naturally, with
no root-kind branch. Manifest-aware verification repeats pruning and the fold
schedule (the threshold must match the proving run), reconstructs every leaf
statement, folds the exact hybrid value, requires no assumptions, and verifies
the same ix_aggr outer claim. Existing non-aggregate proof and
shard-composition verification paths are unchanged.

Claim-only shard preparation

shardCheckEnvClaimTrees constructs only the CheckEnv claim and its
canonical subject/assumption trees. Aggregate planning, shard-digest
reconstruction, and aggregate verification use this path because none of them
consumes dependency bytes. shardCheckEnvClaim remains the witness-building
entry point; it reuses the same claim/tree result and computes the byte
closure once from the union of the owned and available primitive roots.
Reachability distributes over that root union, so this preserves the old
closure while avoiding repeated traversal of shared subgraphs.

Recursion parameters (WP-E1)

Aggregation and aggregate verification consume one shared
MultiStark.RecursionParameters value containing the recursion commitment and
FRI configurations, carried through runAggregateCmdWith/runVerifyCmdWith
into the shared MultiStark.buildRecursionSystem. The CLI entrypoints pass
defaultRecursionParameters, whose fields are exactly today's canonical Aiur
defaults. This is a mechanism split, not a policy change:

  • ordinary IxVM proving and verification remain on the canonical q=100
    configuration;
  • recursion proofs also remain q=100 with the same commitment and PoW values;
  • no new CLI flag selects security parameters; and
  • choosing q=50 plus a compensating PoW policy remains the explicit §9.5
    follow-up decision.

The recursion verifying key serializes and binds both parameter sets, and its
digest is included in every aggregate node's allowed-system blob, so any parameter
change changes the recursive protocol identity without a public-input or
allowed-blob format change. RecursionParameters.cacheFriBytes pins the
40-byte fri_params_ser cache-key component:

logFinalPolyLen:u64-le || maxLogArity:u64-le || numQueries:u64-le ||
commitProofOfWorkBits:u64-le || queryProofOfWorkBits:u64-le

Commitment parameters need no second cache component: changing them changes
the recursion-vk digest already present in the key.

Pruned FRI multiproofs (WP-E2)

The four-commit origin/update-multi-stark series is transplanted onto this
stack: Rust 1.98, the 249b7405 multi-stark revision on P3 v0.6, the matching
Blake3/Lean-FFI pins, and recursive proof-advice encoding. The new proof wire
format deduplicates shared Merkle siblings across sampled FRI queries. Width
and trace-height binding and the vk's checked max_multiplicity travel with
the dependency update; no aggregate claim or public-input format changes.

There are now deliberately two proof encodings. Proof.toBytes is the compact
wire form used by wrappers, native verification, the store, and WP-A cache
entries. Recursive execution still expects one path per query, so callers use
AiurSystem.proofToAdviceBytes immediately before a wrap or pair. The CLI,
pair/typecheck benchmarks, semantic tests, and activation audit all enforce
that boundary. Cached proofs stay compact at rest and are expanded only when
consumed as a parent pair's child.

The dependency's compact format is not backward-decodable. Store-facing CLI
verification therefore uses Proof.ofBytesChecked and reports a normal error
for legacy or corrupt proofs instead of crossing the unchecked decoder and
aborting. P3 v0.6 also requires a positive FRI query count; benchmark CLIs
reject --queries 0, and the old q=0 join smoke gate is replaced by q=1.
The Rust 1.98 migration also applies its new strict-Clippy equivalents for
fixed-size digest chunking and fallible metadata-size lookup; these are
mechanical, semantics-preserving rewrites.

Verified cache and resume (WP-A)

Before the first proof, the driver derives every fold slot's CheckEnv
statement, exact outer Aiur claim, and cache key, using the same
flat/structural fold policy as proving — so a manifest rebudget or statement
change invalidates only the changed subtree. The key:

blake3(
aggregateCacheVersion:u64-le ||
blake3(recursion_vk) ||
recursion_fri_params:40-bytes ||
serializeClaims([outer_claim])
)

The uniform outer claim already includes the ix_aggr entrypoint index and
public input, hence the allowed-system blob, output CheckEnv, both
verifying-key identities, and both accepted entrypoint indices. Each completed
recursive slot is stored as an Ixon.Proof wrapper in the content-addressed
~/.ix/store; a raw --direct-joins leaf is only an input and is not cached.
The wipeable index ~/.ix/cache/aggregate/<key> holds only the wrapper's store
address and is updated via temp-file + atomic rename. M1-d bumps
aggregateCacheVersion from 1 to 2, so old outer-claim entries miss cleanly.

A hit is an untrusted hint, not authority. Reuse requires all of: a
well-formed index address; store bytes whose Blake3 digest equals it; a
decodable wrapper; a bundled claim exactly equal to the precomputed slot
CheckEnv; a safely decoded Aiur proof (Aiur.Proof.ofBytesChecked — the
former trusted-byte constructor panicked on truncated bincode, and malformed
cache bytes must never abort the command); and native verification under the
exact precomputed outer claim and local recursion system. Any failure logs a
miss and re-proves. Cache write failures are warnings. On a valid hit the
host reconstructs the slot's preimages from the manifest statement, so
parents need no trusted metadata from the cache. --no-cache bypasses index
reads and intermediate writes while still persisting the final root wrapper.

RAM-gated parallel scheduling (WP-B)

The post-order fold executes as a dependency DAG: leaves are ready
immediately; a join becomes ready when both child slots have completed. The
controller sorts ready work by descending RAM weight (slot number breaks
ties) and starts one dedicated IO.asTask per admitted slot around the
existing proof FFI. Two independent limits govern admission:

  • --jobs N caps active slots; absent or 0 means every ready slot may run
    if it fits the budget;
  • --max-ram G caps total reserved GiB in flight, defaulting to 92% of Linux
    MemTotal.

As in the existing Rust RamGate, a slot whose estimate exceeds the entire
budget is admitted only while nothing else runs — a conservative estimate
serializes work rather than deadlocking. The calibration-pending per-shape
weights are:

raw IxVM leaf = 4 GiB
shape 0/1 wrap = 195 GiB
shape 2/6 IxVM + IxVM = 390 GiB
shape 3/4/7/8 mixed pair = 340 GiB
shape 5 self + self flat = 195 GiB + 1 MiB × subject leaves
shape 9 self + self struct = 195 GiB

The 195 GiB wrap reserve carries forward the recorded q=100 lift upper bound —
the measured peaks above (186.8–195.8 GiB) bracket it exactly. The 390/340 GiB
direct and mixed estimates deliberately keep the non-default policy out of the
256 GiB fleet tier pending M1-f calibration; measured self-pair peaks
(102.5–156.9 GiB) make their weights conservative in the safe direction. Real
calibration on a known-core box will tighten them without changing scheduler
semantics.

Results are installed by slot index, never arrival order; each worker gets an
immutable snapshot of its completed children, so parallel timing cannot
change parent advice. On failure the controller stops admitting, drains every
running proof, and reports the lowest failed slot; successful independent
tasks may finish publishing valid cache entries. Concurrent proofs share
Aiur's rayon global pool, so throughput can sub-scale even when memory
permits multiple slots; process-per-slot execution is a measurement-driven
follow-up.

Converged activation audit (WP-D + M1-e)

lake exe IxTests aggregate-activation is a dedicated diagnostic kept out of
the default suite. It executes the generated production ix_aggr path across
4 wraps and 128 pairs: two controlled child trace heights, shapes 0–9,
optional assumptions independently per side, complete discharge versus
explicit carry, and all four child-kind combinations in both flat and
structural modes. Every execution returns per-circuit (uniqueRows, totalHits); the runner performs the full 132-case matrix twice and rejects any
difference before rendering a deterministic per-shape Markdown report.
Current signature:

263a3087d42340613b012d50a9416e23549f85aa1ea425e86bf0a216c7b3e27d

Result: 183 audited circuits — 154 active in every case, 29 input-dependent,
zero never observed. The variable set concentrates in the expected paths:
wrap dispatch, canonical set folding, structural Merkle discharge, and
child-kind decoding. Fixed-height Bytes1/Bytes2 circuits are
outside the execute FFI's query-count array and are called out explicitly.
The audit records information only; dummy calls would add cost for the
currently planned SP1 terminal and should be chosen only if a future static
terminal requires input-independent activation.

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.executeMultiStarkJoin for generated or interpreted flat
    and structural join execution;
  • AiurSystem.proveMultiStarkJoin for witness generation and proving;
  • JoinAdvice, JoinPreimage, JoinTree, JoinPath, strict framed decoders,
    and the seven-channel join_io_buffer;
  • the matching rs_aiur_multi_stark_join_execute and
    rs_aiur_multi_stark_join_prove externs; and
  • Aiur.Proof.ofBytesChecked for panic-free store-boundary proof decoding.

The retained three-entrypoint executor is parity-checked against the bytecode
interpreter on both output and every circuit's unique-row/total-hit counts. The
combined verifier contains 247 Aiur functions; the regenerated Rust file is
2,239,894 bytes (ixvm: 5,807,375 bytes / 780 functions). Most line churn is
generated code. The protocol source of truth is
Ix/MultiStark/Aggregate.lean; the native advice contract lives in
aiur_multi_stark_runner.rs. It is now an Ix-agnostic regression/reference backend,
not the production CLI backend.

The production ix_aggr executor is independently parity-checked for wrap,
flat, and structural shapes. It contains 248 Aiur functions and its generated
Rust file is 2,061,361 bytes. Its source of truth is
Ix/Aggr/Circuit.lean, with native advice construction in
aiur_ix_aggr_runner.rs; all three generated artifacts are checked with
ix codegen --check.

Benchmarks

M1-f replaces the legacy two-shard/three-entrypoint runner with
bench-aggregate-policy, a production ix_aggr benchmark for one exact
four-shard fixture. It selects the four retained leaves from a validated and
pruned manifest, contracts the surrounding tree without changing orientation,
and independently reconstructs the expected host root. Wrap-first uses shape 0
leaves; direct mode keeps IxVM leaves raw. Both policies then select their flat
or structural heterogeneous shapes from the production plan. Measured runs
require q=100, jobs=1, --no-cache, and JSON output; the harness natively
verifies all four inputs and every recursive output, persists every completed
slot as an ordinary Ixon.Proof, and writes resumable per-slot and whole-run
timing/RSS/proof metadata.

The existing bench-typecheck --recursive --join row keeps its stable
join-* reporting/dashboard schema, but its join phase now exercises direct
ix_aggr shape 2 instead of the retired join_two circuit. Historical size
baselines remain recorded: 7,986,166 bytes (7,986,204 with the Ixon.Proof
wrapper) for a one-constant q=100 lift under the pre-E2 247-function system,
versus 7,443,023 bytes (7,443,061-byte wrapper) after WP-E2, a 543,143-byte /
6.80% reduction. Pruned multiproof length depends on sampled query-path
overlap, so a positive-PoW output is now a sample rather than an exact
byte-length pin. The deterministic q=100/PoW-0 regression signal is 7,447,279
bytes and 38,850,942,825 recursive-verifier FFT cost. The target-box policy
comparison is the remaining measurement, not remaining harness work.

Test coverage

The legacy aggregate-first suite uses a small stand-in child system so it can
create real Multi-STARK child proofs without the tens to hundreds of GiB
required by a full recursion proof. Those proofs are consumed by the retained
join circuits, so proof validation, statement decoding, set/path folding,
native advice, and transitive recursion are exercised end-to-end at execution
time.

The 57 passing legacy aggregate-first cases cover:

  • canonical flat and structural host folds; manifest parsing, validation, and
    post-order lowering;
  • distinct lift/flat/structural compiled entrypoint identities;
  • honest flat union and cross-child discharge; structural path discharge plus
    a carried assumption;
  • flat-join and structural-join outer claim layout and native verification;
  • transitively pinned flat and structural children; rejection of a child
    carrying a different allowed digest;
  • generated/interpreter parity for both join modes; threshold scheduling that
    is flat below and structural above monotonically;
  • strict keyed-blob framing; wrong-root and tampered-sibling paths; a missing
    path choice; a carried candidate omitted from the output; the obsolete
    88-byte allowed blob; a flat join fed a genuinely free-form structural
    child root; omitted/extra assumptions or subjects; noncanonical unsorted
    trees; tampered child proofs;
  • a synthetic three-leaf manifest with empty outer leaves: raw coverage before
    pruning, contraction, dense remapping, and singleton-lift classification;
  • claim-only shard preparation preserving exact subject/assumption trees,
    with the one-pass witness closure matching the legacy per-owned-root union;
  • exact legacy verifier reconstruction of a lift outer claim and native
    verification under it;
  • byte-for-byte equality of the shared default recursion builder with the
    former direct construction; the exact 40-byte FRI cache encoding;
    independent recursion-vk identity changes for FRI and commitment overrides;
  • cache-key invalidation across version, recursion vk, FRI parameters, and
    outer claim; whole-plan statement/claim/key derivation before proving;
    atomic index creation, corrupt-index detection and repair; a
    content-addressed, claim-bound, natively verified resume hit; mismatched
    claim rejection; corrupt store-content fallback; checked rejection of
    malformed proof bytes without a Rust panic;
  • heaviest-first admission with deterministic tie-breaking; job and byte
    ceilings; dependency release; admit-alone oversize handling; the flat-join
    affine weight and MemTotal parser; jobs=1/jobs=2 payload equality;
    failure draining without admitting a dependent join; jobs=1/jobs=2
    byte-identical recursive wrappers under canonical zero-PoW grinding, with
    each scheduled child decoded and verified; and
  • Rust writer normalization plus round-tripping a genuinely noncanonical
    structural CheckEnv root.

The wrapper-byte scheduler gate uses zero PoW deliberately: with the pinned
positive-PoW implementation, rayon's find_any selects any passing grind
witness, so two valid serial proofs already differ in bytes. Production
jobs=1/jobs=2 correctness is the stronger protocol invariant — exact claims
and native verification under the same recursion system. The primitive
Multi-STARK and recursive-verifier suites were rerun to guard the shared
verifier and lift paths, and the separate activation gate covers 132 accepted
shape/input cases twice.

The focused ix-aggr command now runs 93 passing checks. Its 42
circuit/shape/plan checks cover both wraps, all flat and structural child-kind
pairs, a nested structural self child, generated/interpreter parity, strict
tree and native keyed-blob framing, identity/shape closure, wrong or tampered
paths, omitted carries, unsorted trees, flat↔structural hint confusion, a flat
parent fed a free-form structural root, and exact four-shard wrap-first/direct
plans. Its 51 converged semantic checks cover uniform claims, cache v2 key
invalidation and verified resume, recursion parameters, manifest
validation/pruning, singleton/canonical/hybrid value reconstruction,
shard-preparation closure sharing, wrap-first/direct planning, per-shape RAM
admission, dependency/failure behavior, and jobs=1/jobs=2 determinism including
concurrent zero-PoW proving. Rust unit tests separately pin channel-6 key layout
and strict path-blob framing.

Validation run

All of the following pass on the current stack:

nix develop --command lake exe IxTests aggregate-first # 57/57nix develop --command lake exe IxTests ix-aggr # 93/93nix develop --command lake exe IxTests aggregate-activation # 132 cases × 2 stable passesnix develop --command lake exe IxTests multi-stark # 12/12nix develop --command lake exe IxTests recursive-verifier # 6/6nix develop --command lake build IxTests bench-typecheck bench-recursion-debug bench-aggregate-policy ixnix develop --command cargo test -p ixvm-codegen aiur_multi_stark_runnernix develop --command cargo test -p ixvm-codegen aiur_ix_aggr_runnernix develop --command cargo test -p ix-kernel shard::tests # 23/23nix develop --command cargo test -p ixon test_check_env_claim_accepts_structural_rootnix develop --command cargo clippy -p ixvm-codegen -p ix-ffi -p ix-kernel -p ixon --tests -- -D warningsnix develop --command cargo fmt --all -- --checknix develop --command lake exe ix codegen --checkgit diff --check

Both benchmark entrypoints also reject --queries 0 with a usage error before
building or proving, and a stored pre-E2 proof now reports a checked decode
error rather than aborting. The fresh post-E2 aggregate fixture verifies
against both its .ixe and .ixes inputs.

A q=1 live smoke of the converged typecheck join also executed, proved, and
verified direct shape 2 successfully: 1.221742 s execution, 26,738,665,748 FFT
cost, 52.380108 s proving, 21,565,714,432-byte peak RSS, 401,340-byte proof,
and 0.002616 s verification. The M1-f policy harness passed plan-only smoke for
both policies and emitted the expected resumable JSON schema.

Pre-convergence real-environment gates on the 124 GB development host (kept
as WP-A/WP-B baselines for the M1-f rerun):

  • A two-shard manifest from the 5,986-constant tc-parity.ixe environment
    passes coverage and structural scheduling with threshold zero
    (2 lifts + 1 binary joins (1 structural)), and its one-shard variant
    plans 1 lifts + 0 binary joins.
  • Before E2, a one-constant q=100 production lift ran end-to-end through the
    cache and scheduler, establishing WP-A's invalidation/repair baseline:
PassResultWall
Initialproved and cached root 090bea6f…ca1535~64 s
Identical rerunverified cache hit, same root1.66 s
Truncated indexclean miss, re-proof, atomic repair, same root~38 s
Post-repair rerunverified cache hit, same root1.79 s
--jobs 2 --max-ram 400195 GiB reserved, verified hit, same root1.45 s

At scale, the measured pre-E2 4-shard Init run in Measured performance
above exercised the former backend with real shard proofs, four q=100 lifts,
two lower joins, and a root join with genuine assumption discharge. The pinned
M1-f harness will repeat this fixture under both converged policies.

Soundness summary

  • Every present child is fully verified under the verifying key selected by
    its shape-constrained kind.
  • Child function indices are constrained to IxVM verify_claim or ix_aggr,
    and every recursive child must carry the same allowed digest, pinning the
    two verifying keys and two entrypoints transitively.
  • Claims and allowed data are Blake3-bound and strictly parsed with no trailing
    bytes.
  • Flat mode reopens canonical roots and proves exact union/difference;
    structural mode proves the exact root-of-roots relation and accounts for
    every unique input assumption through either inclusion or explicit carry.
  • A bogus path, dropped candidate, extra survivor, malformed choice, or
    noncanonical assumption tree rejects. Over-carrying and duplicate subject
    leaves can only weaken the output claim; the CLI requires an unconditional
    final root.
  • Coverage is checked on the unmodified manifest, so pruning cannot conceal a
    hole or overlap; only leaves with an environment-derived owned-constant
    count of zero are removed, and contraction preserves the order and shape of
    retained leaves.
  • A singleton root is a verified shape-0 wrapper, never a raw-proof shortcut;
    the circuit binds its output digest directly to the pinned IxVM child's
    CheckEnv digest.
  • Manifest-aware verification independently repeats pruning, scheduling, and
    value folding, then verifies the one derived ix_aggr outer claim. No
    claimed proof kind or shape is accepted from wrapper metadata.
  • Proving and verification build the recursion system from the same explicit
    parameter object; any parameter change necessarily changes the recursion vk,
    allowed-system identity, and cache key.
  • Cache keys bind version, recursion-vk digest, explicit FRI bytes, and exact
    outer claim, all derived before proving; hits re-hash store content, bind
    the claim, decode without panicking, and verify natively. Corrupt, missing,
    or stale entries are misses, never trusted successes or command failures.
  • A pair is admitted only after both child slots complete; immutable
    snapshots and indexed installation make arrival order irrelevant. The
    controller owns all reservations; failures drain running work before exit.
  • Free-form structural roots are serialized as opaque commitments
    (crates/kernel/src/claim.rs); canonical openings are required only by
    protocol paths that explicitly need them.

Review map

Commit-by-commit review is recommended — each commit is feature-scoped with
its own tests (map in the first comment / commit list).

AreaPrimary filesWhat to review
Legacy regression protocolIx/MultiStark/Aggregate.lean, Ix/MultiStark.leanpre-convergence 96-byte identity and semantic/audit reference surface
Production circuitIx/Aggr/Circuit.lean, Ix/Aggr.lean80-byte identity, uniform claims, shapes 0–9, heterogeneous child pinning, structural path checks
Converged host modelIx/Aggr/Host.leanfree-form structural roots, canonical survivors, path extraction
Host statement modelIx/MultiStark/Host.leanfree-form subject roots, canonical assumptions, path extraction
Manifest parsing + pruningIx/Cli/CheckCmd.leanstrict tree parsing, validate-before-prune, unary contraction, dense remap
Aggregate driverIx/Cli/AggregateCmd.leanwrap-first/direct shape policy, singleton wrap, uniform slot claims, cache v2, per-shape RAM weights, verified DAG resume/failure draining
Root verificationIx/Cli/VerifyCmd.leanone ix_aggr outer claim, value-based environment comparison, manifest-relative hybrid reconstruction
Recursion parametersIx/MultiStark.lean, Ix/Aggr.lean, Ix/Cli/{Aggregate,Verify}Cmd.leanshared config plumbing, deterministic systems, stable FRI cache encoding
Pruned FRI multiproofsIx/Aiur/{Protocol,Semantics/BytecodeFfi}.lean, Ix/Cli/{Aggregate,Verify}Cmd.lean, crates/ffi/src/aiur/protocol.rscompact persisted proof vs expanded recursive advice boundary, checked legacy-proof failure
Cache/store boundaryIx/Store.lean, Ix/Aiur/Protocol.lean, crates/ffi/src/aiur/protocol.rswipeable namespace, checked proof decoding, corrupt-data fallthrough
Shard preparationIx/IxVM/ClaimHarness.lean, Ix/Cli/{Aggregate,Check,Verify}Cmd.leanclaim/tree-only consumers, one-pass multi-root witness closure
Native advicecrates/ixvm-codegen/src/aiur_multi_stark_runner.rsstrict framing and seven-channel key layout
Converged native advicecrates/ixvm-codegen/src/aiur_ix_aggr_runner.rs, crates/ffi/src/aiur/protocol.rsstrict path framing, shared channel-6 key layout, execute/prove passthrough
Generated executorcrates/ixvm-codegen/src/aiur_multi_stark.rsgenerated artifact; verify with ix codegen --check
Converged generated executorcrates/ixvm-codegen/src/aiur_ix_aggr.rsgenerated artifact; verify with ix codegen --check
Manifest writercrates/kernel/src/shard.rsproducer-side omission, tree remap, aggregate metadata
Claim semanticscrates/kernel/src/claim.rs, crates/ixon/src/proof.rsfree-form root contract and codec regression test
TestsTests/Aggr.lean, Tests/AggrSemantics.lean, Tests/AggrActivation.lean, Tests/MultiStark.lean93 production shape/semantic/plan checks, 132-case activation audit, retained Ix-agnostic regressions
BenchmarksBenchmarks/AggregatePolicy.lean, Benchmarks/Typecheck.lean, bench registry/dashboardspinned four-shard wrap-first/direct handoff, resumable JSON, converged direct-shape-2 join metrics, historical lift-size pins

Current limits and non-goals

  • Recursion FRI parameters stay at the q=100 defaults; the q=50 + compensating
    PoW policy is an explicit follow-up decision (the measured costs above are
    therefore the conservative end).
  • Structural roots are manifest-relative. Environment-only verification
    accepts only a bundled value equal to the canonical environment root;
    hybrid roots should be verified with both --ixe and --ixes and the
    proving threshold.
  • Scheduler RAM weights are calibration-pending placeholders (validated in
    the safe direction by the measured run); concurrent proves share the rayon
    global pool, so throughput can sub-scale — process-per-slot is a
    measurement-driven follow-up.
  • Same-key cross-process cache writes degrade to a warning and recomputation,
    never a soundness issue.
  • Dummy-call activation padding is deferred until/unless a static terminal
    circuit is selected; under the SP1 terminal it is pure cost.
  • Aggregate wrappers reuse Ixon.Proof and do not encode the proof system;
    callers must pass ix verify --aggregate.
  • M1-a–f land the converged production circuit, host/native substrate, driver,
    cache, scheduler, value-based verifier, semantic union, widened activation
    audit, and policy benchmark harness. The only remaining M1 work is executing
    the wrap-first/direct comparison on the target box. The pinned M1-f revision,
    inherited M1-e preflight, and both measured policies are one checkout/session;
    its handoff is documented in
    plans/aggregate-first-m1f-large-box-runbook.md.
  • This PR does not adopt cold-circuit grouping and does not implement a
    terminal KZG/SP1 wrapper.

Follow-ups

  1. Calibrate lift/join unit costs on a known-core box, with per-node
    flat-vs-structural attribution, and measure frontier/discharge-path counts
    at Mathlib scale (plan §11.4.1).
  2. Decide the recursion q=50 + PoW policy (§9.5) now that the mechanism
    exists.
  3. Connect the root proof to the selected terminal compression path (E3:
    single-recursion-proof SP1 guest + ix compress-root).
  4. Re-evaluate cold-circuit grouping (E4); E2's measured outer-lift reduction
    was only 6.80%, so grouping may still be material.
  5. Process-per-slot proving if measured rayon contention warrants it.
  6. Run the exact M1-f commit
    b70042d6d1a029bb8e1d541da7aef5e0613d67e6 for the combined M1-e preflight
    and wrap-first/direct policy benchmark session in the large-box handoff
    runbook.

johnchandlerburnhamand others added 22 commits August 27, 2026 16:21
Add the deterministic lift/flat/structural activation matrix and keep dummy calls deferred. Split aggregate recursion commitment/FRI configuration from IxVM defaults, share it between proving and verification, and pin the future cache encoding without changing active protocol parameters.
Precompute versioned per-slot cache keys, persist lift and join wrappers in the content-addressed store, and reuse entries only after exact claim and native outer-proof verification. Add safe proof decoding, corruption recovery, and --no-cache.
Execute ready lift and join slots as a dependency DAG under explicit job and RAM admission. Add calibration-pending slot weights, failure draining, CLI controls, and serial/parallel scheduler and proof-equivalence gates.
Add an opt-in two-child benchmark that proves singleton CheckEnv shards, lifts both proofs, and measures a verified flat join. Wire join metrics through reporting and dashboards, and record the current pre-E2 lift-size baseline.
- rust-toolchain.toml channel: 1.92 → 1.98, with the matching fenix
toolchain hash in flake.nix. The pinned fenix already carries the 1.98
release manifest, so flake.lock needs no change (fenix's nixpkgs stays
pinned via its lean4-nix follows, so the Lean toolchain is untouched).
- Drop clippy::from_iter_instead_of_collect from the workspace lints:
removed in clippy 1.98 and now warns as unknown.
- Fix the warnings new clippy 1.98 lints surface across the workspace:
chunks_exact(N) → as_chunks::<N>() where the chunk size is constant,
descending sort_by → sort_by_key(Reverse(..)), iteration over map
values via .values(), map().unwrap_or() → map_or(), a checked
division, an unwrap-after-is_some restructured into if-let, and
assorted redundant-reference/pattern cleanups (mostly cargo clippy
--fix). The two byte-gadget files keep their chunks_exact warnings
until the next commit, which rewrites those regions anyway.
CI derives its Rust version from rust-toolchain.toml, so no workflow
changes are needed.
Companion to multi-stark's update-p3 branch (c72d321 → 249b740), which
carries four soundness/robustness fixes and the Plonky3 v0.6.0 bump
(pruned FRI Merkle multiproofs: ~2x faster verification, 40-70% smaller
proofs; canonical Goldilocks serde removes proof-byte malleability).
Proofs and verifying keys are not compatible with the previous pin.
Integration:
- Lookup gained max_multiplicity, a declared per-row bound on the
multiplicity's integer magnitude feeding the newly enforced logUp
height bound Σ wᵢ·hᵢ + |claims| < p. Function-circuit slots accumulate
mutually-exclusive branch selectors, so they declare 1; committed
count columns (function return slots, the memory circuit, the byte
gadget tables) declare the new COUNT_COLUMN_BUDGET (2^32 queries per
entry).
- The VK wire format carries the bound: u64 LE max_multiplicity per
lookup, between the multiplicity node id and the arg count. The
in-circuit VK deserializer (Ix/MultiStark/SystemDeserialize.lean)
parses past it; the value is bound through the vk digest but the
height bound itself is not yet enforced in-circuit (the native
verifier enforces it).
- Message fingerprints are width-bound by default upstream (the slot
width seeds the Horner fold), which is incompatible with aiur's
branch-shared lookup slots: mutually exclusive branches superpose
messages of different natural widths into one slot at the maximum
width, so a narrow call is sent zero-padded to a width its callee's
return slot never provides, and proving fails with
UnbalancedChannel (pinned as the prove_verify_mismatched_call_widths
regression). Aiur instead declares WidthBinding::ByConstruction —
the plain Horner fold, restoring zero-padding transparency — and
takes on the prefix-freeness contract that makes it sound: every
message's natural width is a function of its constant-constrained
leading prefix (channel tag plus discriminator: fun_idx fixes
2+in+out, the memory size coordinate fixes 3+size, each gadget tag
fixes its table width), so zero-extension can only equate a padded
message with its own natural form. The contract is documented at the
channel constants in lib.rs; the declaration is applied in
AiurSystem::build and mirrored in the vk_codec decoder so decoded
VKs replay the same transcript. ByConstruction is also exactly the
fold the in-circuit verifier's logup_fingerprint already computes,
so the recursive verifier needs no fingerprint change.
- The policy is Fiat-Shamir-bound as the first observe_shape word; the
in-circuit transcript replay prepends the matching limb.
- aiur_multi_stark.rs regenerated (ix codegen) for the deserializer
and transcript changes.
- P3 v0.6.0 ships FRI query openings as pruned Merkle multiproofs,
while the in-circuit verifier consumes one authentication path per
query (its per-query control flow is a far smaller circuit than the
amortized multiproof walk). Rather than porting the walk into the
DSL, the proof advice stays in the per-query transport: multi-stark's
new advice module re-encodes a natively-verified proof by running
p3's own verification with a recording compression function and
reading each query's path back out of the recorded digest map. The
advice bytes are untrusted verifier input, never digest-bound —
the transcript binds the commitments and every expanded sibling is
authenticated against them per query — so pruning vs expansion is
pure transport and the encoding choice is sound. AiurSystem gains
proof_to_advice_bytes (FFI: AiurSystem.proofToAdviceBytes); the
recursive-verifier test feeds it instead of Proof.toBytes, whose
native wire format is still round-tripped separately. The Lean-side
proof grammar and the codegen'd verifier are byte-identical to
before — no in-circuit changes.
Claim layout, the VK wire format above and aiur's public semantics are
otherwise unchanged; the policy adds no prover or verifier work over
the previous pin.
Still open, native-verifier-only: the logUp height bound is parsed
past but not yet enforced in-circuit (a wide-arithmetic check, tracked
separately).
The Lean v4.33.1 update pinned both dependencies at revisions that
predate their Rust 1.98 bumps; their heads now carry those bumps, which
this workspace needs since rust-toolchain.toml moved to 1.98. Both
revisions stay on leanprover/lean4:v4.33.1.
- Blake3.lean 1b0fbd2 → e6e908b (Rust 1.98, plus a case-insensitive
source-directory fix), updated in lakefile.lean, lake-manifest.json
and the blake3-lean flake input. The revision keeps the
`blake3_rs_shared` target the `ix_native_decide_dynlib` pin requires.
The inherited entry in Benchmarks/Compile/lake-manifest.json was still
on the pre-4.33.1 revision and now tracks the root pin.
- lean-ffi 2a9c91e → 93c7e52 (Rust 1.98). Only bignat reaches the sp1
and zisk workspaces, so their lock files move that one package.
The !benchmark recursive phase reported n/a for every fri-verifier
metric: Benchmarks/Typecheck.lean still fed Proof.toBytes — the pruned
multiproof wire format — to executeMultiStark/proveMultiStark, so the
in-circuit verifier rejected on parse and the harness (correctly) left
the recursive fields absent rather than emit a fake datum. The
in-circuit verifier consumes the per-query advice transport
(AiurSystem.proofToAdviceBytes); proofBytes stays the reported
proof-size metric.
bench-recursion-debug had the same advice-format gap plus a stale
claim recipe: it still built the public input as 32 raw digest bytes,
predating the ClaimHarness.packedDigestKey packing bench-typecheck
uses (its own out-of-circuit sanity check failed with
InvalidPowWitness — a wrong claim diverges every challenge — and the
advice re-encoder refused the proof for the same reason). Both aligned
with the typecheck flow.
Validated end-to-end at production parameters (numQueries 100,
query PoW 20, blowup 2) on Nat.add_comm: inner prove, advice
re-encoding, and the codegen'd in-circuit verifier accepting.
Keep compact proof bytes at storage and cache boundaries, expand them only for recursive lift/join advice, reject unsupported zero-query benchmarks, and decode legacy store proofs without panicking. Refresh tests, benchmark pins, and Rust 1.98 lint compatibility.
Add Ix/Aggr, a recursive aggregation system for IxVM shard proofs that
keeps Ix/MultiStark untouched and Ix-agnostic. One entrypoint, ix_aggr,
subsumes lifting and joining: a one-byte advice shape selects wrap or
binary join over any mix of IxVM and ix_aggr children, so shard proofs
enter the recursion system directly as join children and the dedicated
lift stage disappears.
Circuit (Ix/Aggr/Circuit.lean): every shape verifies its children in
full (verify + ood_verify from the shared Multi-STARK verifier modules)
against the vk its hinted kind demands. Identity is one 80-byte
digest-bound blob - blake3(ixvm vk) || verify_claim idx ||
blake3(self vk) || ix_aggr idx - carried unchanged at every node; self
children must bind the identical blob digest, pinning both vks and both
entrypoint indices transitively. Claims are a uniform 18-word
[0, aggr_idx, allowed(8), checkEnv(8)] at every depth, so proofs of
different tree levels combine freely. Wrap shapes bind the output
digest to the child CheckEnv digest directly; pair shapes open both
CheckEnv preimages, re-root the canonical subject/assumption trees, and
prove subjects = L ∪ R, assumptions = (asmL ∪ asmR) ∖ subjects with
linear sorted merges.
Toplevel (Ix/Aggr.lean): ixAggr = MultiStark.multiStarkFull + circuit,
pruned to ix_aggr, so verify_multi_stark_proof and other unrelated
entries no longer pad aggregate proofs. The host half of the wire
contracts (allowed blob, public input packing, shape codes, keyed
preimage/tree blob framing, interpreter IO assembly) lives beside the
toplevel; Ix/Aggr/Host.lean folds CheckEnvTrees statements.
Native path: ix codegen gains the ix-aggr target
(crates/ixvm-codegen/src/aiur_ix_aggr.rs, 244 fns); its runner builds
the seven-channel IO buffer natively and routes execution through the
generated code. New FFI rs_aiur_ix_aggr_execute/_prove
(executeIxAggr/proveIxAggr) pass proofs, vks, claims, and the compact
count/key/length preimage/tree blobs without per-byte boxing, plus
Proof.ofBytesChecked for store-boundary decoding.
CLI: ix aggr --ixe E --ixes M <shard-proof>... reconstructs every
nonempty shard statement from the env, matches wrappers by claim
digest, natively pre-verifies them, folds a balanced bisection (the
canonical fold makes the root claim independent of tree shape), wraps
single-shard roots so the persisted root is always an ix_aggr proof,
checks the root closes over the env canonical tree with no residual
assumptions, and persists the wrapper.
Tests (lake test -- ix-aggr, 17 cases): all five shapes accept over
real Multi-STARK stand-in child proofs from two distinct-vk systems;
codegen'd execution matches the interpreter on output and per-circuit
query counts for wrap and pair; negatives break one binding each -
lying shape hint, tampered proof, foreign identity, wrap statement
drift, dropped assumption, padded subject set, and tree advice not
reproducing its keyed root.
Replace the legacy lift/join entrypoints with the heterogeneous ix_aggr system across aggregate, verification, cache, and codegen paths. Preserve wrap-first and direct-join policies behind one proof identity and cache namespace.
Add the converged 91-check semantic suite and a deterministic 132-case activation audit covering shapes 0 through 9 twice, with no unobserved circuits.
Replace the legacy two-shard three-entrypoint benchmark with a four-shard manifest-subtree harness for production ix_aggr wrap-first and direct policies. Enforce the q=100 serialized no-cache profile, natively verify every input and recursive output, persist ordinary aggregate wrappers, and emit resumable per-slot JSON metrics.
Port bench-typecheck --join to ix_aggr direct shape 2 while preserving the stable join metric schema. Remove the final legacy slot-spec/preimage shim and pin both M1-f four-shard plans in the focused suite.
Update the multi-stark dependency and Rust toolchain for Plonky3 0.6, along with the Rust 1.98 lint migrations required to keep the workspace warning-free. Refresh the Rust-compatible Blake3.lean pin in both root and compile-package manifests.
Adapt recursive Aiur verification to Plonky3's pruned FRI multiproofs. Native proofs retain their compact serialized representation and native verification path; the FFI expands authenticated Merkle frontiers into per-query advice only when entering the existing recursive verifier circuit.
Preserve the packed claim-digest convention in the recursion diagnostic and exercise the advice boundary in the end-to-end test and benchmark paths. CPU and CUDA recursive q1 runs produce identical 823,485-byte inner proofs and 331,273-byte outer proofs.
The q50 Vector.extract_append workload retains identical CPU/CUDA proof sizes. Inner plus outer STARK proving measures 65.87s on CPU and 8.81s with CUDA on the RTX PRO 6000, a 7.48x speedup.
PR benchmark runs execute trusted workflow YAML from the default branch while loading composite actions from the PR checkout. When Bencher data and binary caches are unavailable, the workflow checks out main under base/ and asks Lake to rebuild it without first installing the Rust channel pinned by that checkout.
Teach the existing CPU provenance action to install the base checkout's validated Rust channel and profile immediately before an uncached base build. The step is a no-op when the toolchain is already available and leaves cached benchmark comparisons unchanged.
Consume the Plonky3 0.6 batch-opening layout directly in Aiur instead of expanding every pruned Merkle frontier into one authentication path per FRI query. Sample all query indices from the unchanged transcript, sort and deduplicate them with an O(q log q) merge sort, authenticate each input and commit-phase commitment once, then retain the existing per-query reduced-opening and FRI arithmetic.
Bind every frontier to transcript-derived indices, consume boundary digests in Plonky3's level/parent/child order, reject trailing frontier elements and inconsistent duplicate leaves, and assert all native opening dimensions and sibling counts. Explicitly constrain the digest-bound protocol specialization to cap height 0, binary FRI, and a constant final polynomial. Move memo_u32_less_than into IxVM Core so both substitution and multiproof sorting share its constrained rows.
Strengthen the recursive negative test to mutate a structurally valid stage-1 commitment. Regenerate both checked-in Aiur Rust executors and retain interpreter/codegen query-count parity.
On Vector.extract_append q50, recursive-verifier FFT cost falls from 204.073B to 201.166B. CPU outer proving improves from 50.09s to 45.03s and the full CPU pipeline from 90.64s to 82.90s. GPU outer proving improves from 15.85s to 13.72s and the full GPU pipeline from 28.86s to 26.69s. The outer proof grows from 3.92 MB to 4.17 MB.
Validated with the MultiStark primitive suite, recursive honest/tamper/parity tests, codegen --check, release workspace clippy, release CUDA clippy, rustfmt, and diff checks.
@johnchandlerburnham

Copy link
Copy Markdown
MemberAuthor

Init full proving report

Revision: 82dcc9dba36d0cfd6c753c6878ab649643a52026
Host: 64 vCPU, 495 GiB RAM, 256 GiB swap
Final result: one verified ix_aggr FRI proof with no remaining assumptions.

Compilation and sharding

StepResult
Compile sourceBenchmarks/Compile/CompileInit.lean
Ix compiler time1.876s
Constants compiled65,994
init.ixe size194,296,853 bytes (185.3 MiB)
Static sharding time3.1s
Manifest16 nonempty shards
Retained/owned constants56,621
Manifest size2,619,404 bytes (2.50 MiB)
Predicted FFT per shard4.160e11–4.197e11
Predicted FFT spread1.01×

The 1.876-second compile measurement is the Ix compiler itself; it excludes the initial one-time dependency download and Lake build.

Stage 1: base shard proofs

All 16 shard proofs were persisted and individually verified against their corresponding manifest claims.

MetricSerial baselineTwo-way run used for aggregation
Concurrency12
End-to-end wall time24m 16.76s15m 53.93s
Sum of shard prover times22m 43.23s30m 08.14s
Per-shard time range74.08–113.97s98.94–142.29s
Median shard time82.66s108.75s
Highest individual RSS219.8 GiB219.8 GiB
Combined process-tree peak358.2 GiB
Proof count1616
Total proof-wrapper size369,995,038 bytes
Mean proof-wrapper size23,124,690 bytes
Proof-wrapper range22,714,520–23,998,692 bytes

Two-way concurrency gave a 1.53× wall-time speedup despite increasing total prover-seconds by about 33% from CPU/memory contention.

Stage 2: recursive aggregation

Policy:

  • Wrap-first
  • --structural-above 4096
  • --jobs 2
  • --max-ram 450
  • Default q=100 recursion parameters
  • Aggregate cache enabled

Plan:

OperationCount
Shard wraps16
Binary joins15
Structural joins14
Flat joins1
Total recursive slots31

Results:

MetricResult
Wall time45m 06.61s
Peak resident memory379.9 GiB
Average CPU utilization4,192% (~42 cores)
Cache hits0
Freshly proved/cached slots31
Swap used0 bytes
Exit status0
Final proof-wrapper size9,596,044 bytes (9.15 MiB)

Final proof

Root address:

80f6844f968920abaef8d8c750a1a1dc1ebe32657a3cfb824e1810bcd65c8814

Final claim:

CheckEnv(6dc8f821a989c6f7b9d520b053ef8edbd582d2408d7b6a6ff72b908666e03b6f, none)

Both validations passed:

  • Cryptographic-only aggregate verification
  • Manifest-aware verification against init.ixe, init-16.ixes, and structural threshold 4096

End-to-end measured time

Using the two-way Stage 1 run:

ComponentTime
Ix compilation1.876s
Sharding3.1s
Stage 1 proofs15m 53.932s
Stage 2 aggregation45m 06.61s
Measured core pipeline1h 01m 05.52s

This total excludes the one-time dependency build/download and a few seconds of manifest and final-root verification overhead. Using serial Stage 1 instead would make the measured core pipeline approximately 1h 09m 28.35s.

Use the PR77 lean-ffi and multi-stark pins, remove the stale width-binding and multiplicity-bound protocol fields, and regenerate the recursive and aggregate verifier sources.
Expose an opt-in x86_64-linux cuda shell with NVCC 13.2, sm_120 code generation, and safe host libcuda discovery while leaving the default development shell unchanged.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@johnchandlerburnham@samuelburnham@arthurpaulino