Skip to content

feat(plonky3): Phase 0 recursion feasibility spike — gate is GO (carrier tables) - #212

Merged
TaprootFreak merged 11 commits into
stagingfrom
feat/plonky3-p0-recursion-spike
Jun 6, 2026
Merged

feat(plonky3): Phase 0 recursion feasibility spike — gate is GO (carrier tables)#212
TaprootFreak merged 11 commits into
stagingfrom
feat/plonky3-p0-recursion-spike

Conversation

@TaprootFreak

@TaprootFreak TaprootFreak commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Executes Phase 0 of the Plonky2 → Plonky3 migration (#211, MIGRATION_PLONKY3.md §5) — the hard recursion-feasibility gate. An isolated throwaway spike crate (real proving + pos/neg assertions) exhaustively validates the recursion model on the pinned revs.

🟢 Gate result: GO via Path 1+5 (carrier tables)

This PR's probes established the recursion mechanism and the primitive-table limits. An initial reading of those limits was NO-GO; it was overturned — see solution research #213 and the end-to-end carrier-chain proof #214. The probes here remain correct for the constructions they tested; the overturning came from a construction they did not test (a custom public-value-emitting AIR). Full reasoning in MIGRATION_PLONKY3_SPIKE_RESULT.md.

The cross-layer state channel — zkCoins' prev_account/ProofData carry — is buildable on this Plonky3-recursion rev via carrier tables: a custom AIR with num_public_values() > 0 exposes a soundly-bound per-instance value across a batch-recursion layer (upstream PR #407, in our pinned rev). Proven end-to-end in #214 (probe_q + probe_r_carrier_chain: depth-4 IVC chain threading V_3 == V_0+3, sound negatives, no fork).

What this PR's probes show

p3-recursion is a layered batch-stark model. Primitive tables and raw CircuitBuilder public inputs do NOT carry state across a layer — these probes pin that, and they remain as regression guards so the port routes state through carrier tables, not raw public inputs:

  • probe_d_multilayer_carry (air_public_targets = [0,0,0]), probe_h_option1_air_public_values (injecting table_public_inputs rejected), probe_g_fanin_pi_passthrough (stock aggregation surfaces 0 per-leaf values).
  • probe_j_option2_rebind: the in-circuit Poseidon2 hash-bind primitive works (but a stock layer can't read a predecessor's committed digest — which is exactly why the carrier-table public-value channel, not a digest re-read, is the right construction).

Probes (17 here; +3 in #214 = 20 total)

Binding primitives (proven in isolation): probe_a/b/c/d_pi_threading/e/f — IVC fixed point, fan-in tree, PI binding, single-hop threading, §7.17 active-bit masking, vk-equality connect-back.
Primitive-table guards (pinned = 0): probe_d_multilayer_carry, probe_h, probe_g, probe_j.
Cost: probe_i (≈3.2 s/layer at 2^16-gate scale).
Robustness: probe_l (multi-AIR), probe_m (depth-50 fixed point, flat 1.4 GB), probe_n (concurrency), probe_o (soundness — verifier non-vacuous), probe_p (proof serialization, ~363 KB byte-stable).
Overturning + chosen direction (in #214): probe_q_custom_public_value, probe_r_carrier_chain, probe_r_cost.

Pins

  • Plonky3/Plonky3-recursion @ 524665d0c2e1d294722c064786ae11dff8d9f33b
  • Plonky3/Plonky3 @ 56952503e1401a62982ceaf952c5e4a829b61803

Notes

  • Companion to plan #211 (records the GO + carrier-table direction). Spike is its own workspace, excluded from node/shared CI; fmt + clippy -D warnings clean.
  • The spike (≤1-week investment) answered the gate before any porting started.

Add spikes/plonky3-recursion-spike, an isolated probe (its own workspace,
excluded from the root workspace so the heavy Plonky3 git deps never enter the
node/shared build or CI) that empirically proves Plonky3/Plonky3-recursion can
express the three composition patterns the zkCoins circuit depends on, in
Goldilocks, on the pinned revs:

- Probe A (IVC/cyclic with base case): 4-layer chain; the verifier-circuit shape
  reaches a fixed point (true IVC, no growth) — the analogue of Plonky2
  common_data_for_recursion. witness_count [25567, 104630, 107957, 107957].
- Probe B (fan-in-8, variable active count): 2-to-1 aggregation composes into a
  fixed-shape tree (fan-in-4 probed; fan-in-8 is one more level). No native
  conditional-verify primitive; inactive slots are padded with real proofs and
  masked downstream via an active bit.
- Probe C (vk/PI binding): an inner proof's public inputs are bound in the
  verifier circuit; a mismatched claim is rejected in-circuit.

Record P0-T5 cost (~4.65 s per stabilized layer, ~1 GB peak RSS) and the P0-T6
Go/No-Go memo (MIGRATION_PLONKY3_SPIKE_RESULT.md). Gate decision: GO.

Pins: Plonky3-recursion 524665d0c2e1d294722c064786ae11dff8d9f33b,
Plonky3 56952503e1401a62982ceaf952c5e4a829b61803.
…a exactly

Address logic-review findings — scope the spike's claims precisely against
MIGRATION_PLONKY3.md §5 so the GO gate is not overstated:

- Probe A: strengthen the fixed-point assertion (require the shape to have GROWN
  before stabilising, not just last-two-equal). Document that cross-layer counter
  PI THREADING (P0-T2 crit. 2) is not exercised — into_recursion_input carries
  empty table_public_inputs; the enabling primitive is proven in Probe C; explicit
  threading is Phase-5 work.
- Probe B: relabel honestly — the probe proves 2-to-1 fan-in TREE COMPOSITION
  (4 identical real leaves), NOT variable active count / per-leaf PIs / active-bit
  masking. The masking strategy (§7.17) is Phase-5 construction on proven primitives.
- Probe C: relabel as PUBLIC-INPUT binding (proven), with vk-equality connect-back
  as Phase-5 construction (the literal "wrong-vk proof" is not fed).

Memo gate decision restated: the three mechanisms are proven; the three deferred
items are in-repo Phase-5 construction, not upstream gaps. Decision stays GO.

All 4 probes still green; fmt + clippy clean.
…/E/F)

The Phase-0 gate previously proved the three mechanisms but DEFERRED the three
real §5 PASS items to Phase 5. This adds probes that exercise them end-to-end
with real proving and positive+negative (+control) assertions:

- Probe D (probe_d_pi_threading): cross-layer PI threading binding. An outer
  circuit verifying an inner uni-stark proof exposes air_public_targets and
  threads a value (next_start = inner.last + 1) bound to an outer public input;
  a wrong threaded value is rejected; a control without the bind accepts it.
- Probe D part 2 (probe_d_multilayer_carry): the escalated finding. Verifying an
  inner BATCH proof of a CircuitBuilder circuit exposes NO inner public inputs
  (air_public_targets = [0,0,0]); the high-level chain does not propagate public
  inputs across layers (differs from Plonky2 cyclic recursion). Pinned by assert.
- Probe E (probe_e_active_masking): variable-active-count masking (§7.17). An
  8-slot fixed-shape consumer circuit, batch-stark-proved for real: active+correct
  with inactive-garbage accepted (masked); active-wrong rejected; flipping a
  garbage slot's active bit flips the verdict; flipping back re-masks.
- Probe F (probe_f_vk_binding): vk-equality connect-back. Two ConstPrepAir
  instances (k=42/99) have different preprocessed commitments (= vks). A proof
  from vk_99 (internally valid against vk_99) is rejected SOLELY by the connect to
  vk_42; a control accepts it unbound.

Rewrite MIGRATION_PLONKY3_SPIKE_RESULT.md: the §5 items are now exercised (not
deferred); the gate is GO with ONE escalated finding (cross-layer public-input
propagation), surfaced for operator judgment per §16. All 8 probes green; fmt +
clippy clean.
@TaprootFreak
TaprootFreak marked this pull request as ready for review June 6, 2026 12:04
… G/H/I)

Three integrated probes that resolve the previously-escalated open questions into
hard, pre-Phase-1 constraints:

- Probe H (probe_h_option1_air_public_values): Option 1 (carry the threaded value as
  an AIR public value) is DEAD. The honest empty-PI layer builds+proves; injecting a
  non-empty RecursionInput::BatchStark.table_public_inputs is rejected. Combined with
  probe_d_multilayer_carry (air_public_targets = [0,0,0]), both Option-1 avenues fail.
- Probe G (probe_g_fanin_pi_passthrough): a real 2-to-1 aggregation's per-leaf values
  are NOT surfaced to the outer (air_public_targets = 0). The integrated fan-in-8
  per-leaf-PI passthrough is blocked at the first cross-layer hop.
  => Together G+H decide the Phase-1-authorize choice: Option 2 (commit + hash re-bind)
  is MANDATORY for IVC threading AND aggregator per-leaf surfacing.
- Probe I (probe_i_cost_projection): recursion-layer cost at real inner-proof scale.
  Sub-linear scaling; a layer over a ~2^16-gate proof is ~3.2s / ~1.4GB. Combined with
  the mandatory Option-2 overhead and the base prove, the 5s warm budget is at material
  risk -> measure on the real circuit early in Phase 5.

Rewrite MIGRATION_PLONKY3_SPIKE_RESULT.md: status is now CONDITIONAL GO (Option 2
mandatory; warm-prove budget at material risk). The pinned probes (G/H/multilayer_carry)
catch a future upstream rev that restores native public-input propagation. All 11
probes green; fmt + clippy clean.
…cannot compose

The in-circuit Poseidon2 hash-bind primitive (add_hash_slice + connect) is real and
binding: hash(V) binds to hash(V); a mismatched preimage is rejected. So Option 2's
per-layer commit+rebind building block is expressible. But it needs layer N+1 to read
layer N's committed digest, which is structurally impossible across a batch layer
(probe_d_multilayer_carry / probe_g / probe_h: no per-instance value exposed, only
whole-trace Merkle commitments). So multi-layer Option-2 threading is NOT achievable —
confirming both Option 1 and Option 2 are dead and the cross-layer state IVC is
unbuildable on this rev.
…ayers (Probe J)

Probe J + an adversarial review of all six escape routes confirm that neither Option 1
nor Option 2 can thread a value across a batch-recursion layer. The per-layer
commit+rebind primitive works (in-circuit Poseidon2 hash-bind), but layer N+1 cannot
read layer N's committed digest (batch proofs expose only whole-trace Merkle commitments;
FRI openings are FS-random; vk is per-circuit-static; NPO public_values are hardcoded
empty with no public registration path). So zkCoins' cross-layer state IVC is
structurally unbuildable on this rev. Memo status: CONDITIONAL GO -> NO-GO, with escape
routes (upstream feature / protocol redesign / fork-excluded).
…ndness

- Probe L (probe_l_multi_air): two heterogeneous AIRs (CounterAir state-transition-like
  + ConstPrepAir aggregator-like) co-verify in one verifier circuit, public inputs kept
  distinct + individually bound; cross-wiring A's PI to B's value is rejected.
- Probe N (probe_n_concurrent): 4 independent prove+recurse+verify workloads on separate
  threads all succeed; peak RSS ~1.38 GB. Prover is concurrency-safe.
- Probe O (probe_o_soundness): soundness spot-check — mismatched FRI private data (a
  different proof's Merkle paths) is rejected by the in-circuit verification, and a
  tampered public-input claim is rejected. Confirms the negatives in C/D/F/J/L are
  genuine rejections, not vacuous acceptances.
- Probe M (probe_m_long_chain) added (50-layer IVC chain, fixed-point-holds-at-depth);
  runs slow, result folded into the memo separately.

These validate recursion-mechanism robustness (multi-AIR, concurrency, soundness, depth)
for a future re-evaluation; they do not change the NO-GO (cross-layer state threading is
still unbuildable).
…ence)

A recursion proof bincode-serializes to ~363 KB, round-trips byte-stable, and the
deserialized proof still verifies; a truncated blob is rejected. Adds a verify_batch_proof
helper. Relevant to Phase 6 proof-blob storage. Does not change the NO-GO.
…th-50, concurrency, soundness, serialization)
@TaprootFreak TaprootFreak changed the title feat(plonky3): Phase 0 recursion feasibility spike — GO gate feat(plonky3): Phase 0 recursion feasibility spike — gate is GO (carrier tables) Jun 6, 2026
@TaprootFreak
TaprootFreak merged commit d94ccbf into staging Jun 6, 2026
3 checks passed
TaprootFreak added a commit that referenced this pull request Jun 7, 2026
The Plonky3 recursion spike, its migration write-ups, and its benchmark
results were merged to staging only (PRs #211/#212/#214) and must not be
promoted to develop. Remove them here so the next staging -> develop
auto-promote carries no Plonky3-migration artifacts. Everything removed is
archived verbatim in zk-coins/research.

- delete the plonky3-recursion-spike crate (36 files)
- delete MIGRATION_PLONKY3.md / _SOLUTIONS_RESEARCH / _SPIKE_RESULT
- delete docs/migration/PLONKY3_*.md (5 files)
- delete scripts/bench/results/plonky3-*.md (5 files)
- restore the workspace Cargo.toml to develop's form (drop the now-unused
  `exclude = ["spikes/plonky3-recursion-spike"]`)
TaprootFreak added a commit that referenced this pull request Jun 7, 2026
* docs: Plonky3 migration plan + Phase-0 GO (carrier-table direction) (#211)

* docs: add Plonky3 migration plan with phased, executable task spec

Adds MIGRATION_PLONKY3.md: a phase-by-phase, execution-ready work plan
for the Plonky2 -> Plonky3 backend swap. Phase 0 is a hard recursion
feasibility gate against p3-recursion (probed in Goldilocks to isolate
the recursion/API risk from the field-migration risk). Subsequent
phases port types/hash, Merkle gadgets, the state-transition circuit,
recursion + aggregator, node integration, and parity/coverage/bench,
each with exact files, acceptance criteria, and local verification
commands. Field swap to KoalaBear/BabyBear is sequenced as a separate
optional follow-up. Companion to ROADMAP.md and MIGRATION_RESEARCH.md.

* docs: record Phase 0 result + defer cross-layer PI-threading choice to Phase-1-authorize

The Phase 0 spike returned GO with one escalated finding: the high-level batch
recursion does not propagate public inputs across layers
(probe_d_multilayer_carry: air_public_targets = [0,0,0]), unlike Plonky2 cyclic
recursion. This is a construction problem, not a p3-recursion capability gap, so it
does not flip the gate to NO-GO.

Record this in the Phase 0 gate section, and mark the cross-layer PI-threading
construction as TBD — the choice between Option 1 (threaded outputs as AIR public
values, fast), Option 2 (commit + Merkle/hash re-bind each layer, sound), and
Option 3 (pinned probe catches a future upstream rev that propagates natively) is
made at Phase-1-authorize time, not now. P5-T1 is written against the chosen option
and its acceptance now requires the threaded prev_account value to be carried across
transitions.

* docs: resolve cross-layer threading to Option 2 + record Phase-5 budget risk

Phase 0 closed the open threading question empirically, so the plan no longer
defers it to Phase-1-authorize:

- Cross-layer PI threading is RESOLVED to Option 2 (commit + hash/Merkle re-bind
  each layer). Option 1 (carry the value as an AIR public value) is dead — proven
  by probe_h_option1_air_public_values (injecting a non-existent public input is
  rejected) and probe_g_fanin_pi_passthrough (a real aggregation surfaces 0 per-leaf
  values to the outer); CircuitBuilder public inputs live in the committed Public
  table, never as AIR public values. Reflected in the §5 recorded result, the §6
  Phase-1-authorize block (now "RESOLVED: Option 2"), P5-T1, and P5-T2 (per-leaf
  ProofData also needs Option-2 commit+re-bind, then §7.17 masking).
- Record the Phase-5 warm-prove budget risk from probe_i_cost_projection: a recursion
  layer over a ~2^16-gate inner proof is ~3.2 s / ~1.4 GB — a single-layer lower
  bound on an arithmetic toy; the mandatory Option-2 re-bind adds Poseidon gates per
  layer, and the real constraints are Poseidon-heavy. Measure the real circuit +
  Option-2 at the START of Phase 5; a >5 s warm-prove is a NO-GO trigger.

Option 3 (pinned probes catch a future upstream rev that restores native propagation)
stays armed.

* docs: gate is NO-GO — neither Option 1 nor Option 2 threads state across a batch layer

Phase 0 closed the cross-layer threading question definitively: Option 1 (AIR public
values) is dead (probe_h/g/d_multilayer_carry) and Option 2 (commit + hash re-bind) is
also dead (probe_j + adversarial review — layer N+1 cannot read layer N's committed
digest). There is no per-instance value channel across a batch-recursion layer, so
zkCoins' prev_account/ProofData IVC carry is structurally unbuildable on this rev.

Record NO-GO in the §5 result, the §6 Phase-1-authorize block ("DO NOT START Phase 1";
§6-§14 kept as a would-be plan for if an escape route opens), P5-T1 (BLOCKED), P5-T2
(BLOCKED), and the §10 budget note (moot under NO-GO). Escape routes: an upstream rev
that exposes cross-layer public inputs (pinned probes auto-detect), a protocol redesign,
or a fork (excluded by §16). Decision is the operator's per §16.

* docs(plonky3): gate is GO via Path 1+5 (carrier tables) — overturn NO-GO, unblock Phases 4-5

Phase 0 reassessment: probe_q + probe_r_carrier_chain prove a custom AIR's public
value (carrier table) threads state across a batch layer end-to-end (V_3==V_0+3,
sound negatives, no fork). probe_r_cost: within ≤5s warm budget. Update P0-T6 memo
block, §6 Phase-0 outcome, and P5-T1/P5-T2/budget-note to the carrier-table
direction. Rationale: MIGRATION_PLONKY3_SOLUTIONS_RESEARCH.md; proof: PR #214.

* feat(plonky3): Phase 0 recursion feasibility spike — gate is GO (carrier tables) (#212)

* feat(plonky3): Phase 0 recursion feasibility spike — GO gate

Add spikes/plonky3-recursion-spike, an isolated probe (its own workspace,
excluded from the root workspace so the heavy Plonky3 git deps never enter the
node/shared build or CI) that empirically proves Plonky3/Plonky3-recursion can
express the three composition patterns the zkCoins circuit depends on, in
Goldilocks, on the pinned revs:

- Probe A (IVC/cyclic with base case): 4-layer chain; the verifier-circuit shape
  reaches a fixed point (true IVC, no growth) — the analogue of Plonky2
  common_data_for_recursion. witness_count [25567, 104630, 107957, 107957].
- Probe B (fan-in-8, variable active count): 2-to-1 aggregation composes into a
  fixed-shape tree (fan-in-4 probed; fan-in-8 is one more level). No native
  conditional-verify primitive; inactive slots are padded with real proofs and
  masked downstream via an active bit.
- Probe C (vk/PI binding): an inner proof's public inputs are bound in the
  verifier circuit; a mismatched claim is rejected in-circuit.

Record P0-T5 cost (~4.65 s per stabilized layer, ~1 GB peak RSS) and the P0-T6
Go/No-Go memo (MIGRATION_PLONKY3_SPIKE_RESULT.md). Gate decision: GO.

Pins: Plonky3-recursion 524665d0c2e1d294722c064786ae11dff8d9f33b,
Plonky3 56952503e1401a62982ceaf952c5e4a829b61803.

* docs(plonky3): tighten Phase 0 memo + probes to match §5 PASS criteria exactly

Address logic-review findings — scope the spike's claims precisely against
MIGRATION_PLONKY3.md §5 so the GO gate is not overstated:

- Probe A: strengthen the fixed-point assertion (require the shape to have GROWN
  before stabilising, not just last-two-equal). Document that cross-layer counter
  PI THREADING (P0-T2 crit. 2) is not exercised — into_recursion_input carries
  empty table_public_inputs; the enabling primitive is proven in Probe C; explicit
  threading is Phase-5 work.
- Probe B: relabel honestly — the probe proves 2-to-1 fan-in TREE COMPOSITION
  (4 identical real leaves), NOT variable active count / per-leaf PIs / active-bit
  masking. The masking strategy (§7.17) is Phase-5 construction on proven primitives.
- Probe C: relabel as PUBLIC-INPUT binding (proven), with vk-equality connect-back
  as Phase-5 construction (the literal "wrong-vk proof" is not fed).

Memo gate decision restated: the three mechanisms are proven; the three deferred
items are in-repo Phase-5 construction, not upstream gaps. Decision stays GO.

All 4 probes still green; fmt + clippy clean.

* feat(plonky3): exercise the three §5 PASS items empirically (Probes D/E/F)

The Phase-0 gate previously proved the three mechanisms but DEFERRED the three
real §5 PASS items to Phase 5. This adds probes that exercise them end-to-end
with real proving and positive+negative (+control) assertions:

- Probe D (probe_d_pi_threading): cross-layer PI threading binding. An outer
  circuit verifying an inner uni-stark proof exposes air_public_targets and
  threads a value (next_start = inner.last + 1) bound to an outer public input;
  a wrong threaded value is rejected; a control without the bind accepts it.
- Probe D part 2 (probe_d_multilayer_carry): the escalated finding. Verifying an
  inner BATCH proof of a CircuitBuilder circuit exposes NO inner public inputs
  (air_public_targets = [0,0,0]); the high-level chain does not propagate public
  inputs across layers (differs from Plonky2 cyclic recursion). Pinned by assert.
- Probe E (probe_e_active_masking): variable-active-count masking (§7.17). An
  8-slot fixed-shape consumer circuit, batch-stark-proved for real: active+correct
  with inactive-garbage accepted (masked); active-wrong rejected; flipping a
  garbage slot's active bit flips the verdict; flipping back re-masks.
- Probe F (probe_f_vk_binding): vk-equality connect-back. Two ConstPrepAir
  instances (k=42/99) have different preprocessed commitments (= vks). A proof
  from vk_99 (internally valid against vk_99) is rejected SOLELY by the connect to
  vk_42; a control accepts it unbound.

Rewrite MIGRATION_PLONKY3_SPIKE_RESULT.md: the §5 items are now exercised (not
deferred); the gate is GO with ONE escalated finding (cross-layer public-input
propagation), surfaced for operator judgment per §16. All 8 probes green; fmt +
clippy clean.

* docs(plonky3): mark probe_a row as P0-T2 crit. 1 (not PI threading)

* feat(plonky3): close the cross-layer/cost gaps before Phase 1 (Probes G/H/I)

Three integrated probes that resolve the previously-escalated open questions into
hard, pre-Phase-1 constraints:

- Probe H (probe_h_option1_air_public_values): Option 1 (carry the threaded value as
  an AIR public value) is DEAD. The honest empty-PI layer builds+proves; injecting a
  non-empty RecursionInput::BatchStark.table_public_inputs is rejected. Combined with
  probe_d_multilayer_carry (air_public_targets = [0,0,0]), both Option-1 avenues fail.
- Probe G (probe_g_fanin_pi_passthrough): a real 2-to-1 aggregation's per-leaf values
  are NOT surfaced to the outer (air_public_targets = 0). The integrated fan-in-8
  per-leaf-PI passthrough is blocked at the first cross-layer hop.
  => Together G+H decide the Phase-1-authorize choice: Option 2 (commit + hash re-bind)
  is MANDATORY for IVC threading AND aggregator per-leaf surfacing.
- Probe I (probe_i_cost_projection): recursion-layer cost at real inner-proof scale.
  Sub-linear scaling; a layer over a ~2^16-gate proof is ~3.2s / ~1.4GB. Combined with
  the mandatory Option-2 overhead and the base prove, the 5s warm budget is at material
  risk -> measure on the real circuit early in Phase 5.

Rewrite MIGRATION_PLONKY3_SPIKE_RESULT.md: status is now CONDITIONAL GO (Option 2
mandatory; warm-prove budget at material risk). The pinned probes (G/H/multilayer_carry)
catch a future upstream rev that restores native public-input propagation. All 11
probes green; fmt + clippy clean.

* docs(plonky3): scope Option-1 'impossible' to 'not achievable on this rev'

* feat(plonky3): Probe J — Option 2 commit+rebind primitive works, but cannot compose

The in-circuit Poseidon2 hash-bind primitive (add_hash_slice + connect) is real and
binding: hash(V) binds to hash(V); a mismatched preimage is rejected. So Option 2's
per-layer commit+rebind building block is expressible. But it needs layer N+1 to read
layer N's committed digest, which is structurally impossible across a batch layer
(probe_d_multilayer_carry / probe_g / probe_h: no per-instance value exposed, only
whole-trace Merkle commitments). So multi-layer Option-2 threading is NOT achievable —
confirming both Option 1 and Option 2 are dead and the cross-layer state IVC is
unbuildable on this rev.

* docs(plonky3): gate is NO-GO — Option 2 cannot compose across batch layers (Probe J)

Probe J + an adversarial review of all six escape routes confirm that neither Option 1
nor Option 2 can thread a value across a batch-recursion layer. The per-layer
commit+rebind primitive works (in-circuit Poseidon2 hash-bind), but layer N+1 cannot
read layer N's committed digest (batch proofs expose only whole-trace Merkle commitments;
FRI openings are FS-random; vk is per-circuit-static; NPO public_values are hardcoded
empty with no public registration path). So zkCoins' cross-layer state IVC is
structurally unbuildable on this rev. Memo status: CONDITIONAL GO -> NO-GO, with escape
routes (upstream feature / protocol redesign / fork-excluded).

* feat(plonky3): Probes L/N/O — multi-AIR coexistence, concurrency, soundness

- Probe L (probe_l_multi_air): two heterogeneous AIRs (CounterAir state-transition-like
  + ConstPrepAir aggregator-like) co-verify in one verifier circuit, public inputs kept
  distinct + individually bound; cross-wiring A's PI to B's value is rejected.
- Probe N (probe_n_concurrent): 4 independent prove+recurse+verify workloads on separate
  threads all succeed; peak RSS ~1.38 GB. Prover is concurrency-safe.
- Probe O (probe_o_soundness): soundness spot-check — mismatched FRI private data (a
  different proof's Merkle paths) is rejected by the in-circuit verification, and a
  tampered public-input claim is rejected. Confirms the negatives in C/D/F/J/L are
  genuine rejections, not vacuous acceptances.
- Probe M (probe_m_long_chain) added (50-layer IVC chain, fixed-point-holds-at-depth);
  runs slow, result folded into the memo separately.

These validate recursion-mechanism robustness (multi-AIR, concurrency, soundness, depth)
for a future re-evaluation; they do not change the NO-GO (cross-layer state threading is
still unbuildable).

* feat(plonky3): Probe P — proof serialization round-trip (node persistence)

A recursion proof bincode-serializes to ~363 KB, round-trips byte-stable, and the
deserialized proof still verifies; a truncated blob is rejected. Adds a verify_batch_proof
helper. Relevant to Phase 6 proof-blob storage. Does not change the NO-GO.

* docs(plonky3): record mechanism-robustness probes L-P (multi-AIR, depth-50, concurrency, soundness, serialization)

* feat(plonky3): carrier-table IVC (Path 1+5) + full migration audit + recursion-reduction — GO, /api/send recovers w/o UX regression, port HOLD (#214)

* feat(plonky3): Probe Q — a custom AIR's public value DOES cross a batch layer (overturns NO-GO)

Replicates upstream test_batch_verifier_with_public_values (PR #407, in our pinned rev)
in our crate: a custom PublicValueAir (num_public_values=1) proved with prove_batch and
verified in-circuit via verify_batch_circuit surfaces its public value as a non-empty
air_public_target (NOT [0,0,0]) and binds it soundly across the batch layer — correct
value accepted, wrong value rejected.

This overturns the scoped NO-GO: the [0,0,0] finding (probes D/G/H) held only for the
PRIMITIVE tables and CircuitBuilder public inputs (which route to the committed Public
table). A public-value-emitting AIR provides exactly the per-instance cross-layer value
channel the IVC needs. The full IVC chaining via a custom carrier table is a public-API
construction (~400-650 LOC), not an impossibility.

* docs(plonky3): solution-space research — NO-GO overturned, 9 paths assessed

Probe Q empirically overturns the scoped NO-GO: a custom AIR's public value DOES cross a
batch-recursion layer (PR #407, in our pinned rev). Enumerate + assess all 9 solution
paths with links/repo-pointers: (1+5) Plonky3 + custom public-value-emitting tables —
viable, channel proven, IVC chaining is a public-API construction; (3) folding/Sonobe —
native IVC, strong alternative; (2) self-authored upstream PR; (4) hybrid; (6) protocol
redesign via off-circuit continuity (trusted node, §7.22 posture); (7) zkVMs; (8) fork
(excluded §16); (9) Stwo/Triton/Halo2-accumulation. Recommend Path 1+5 behind a
carrier-table IVC-chain spike (Probe R), with Sonobe benchmarked in parallel.

* docs(plonky3): address review — add ProtoStar/Boojum/Lasso, gate-memo forward-pointer

- Solutions doc: add ProtoStar/ProtoGalaxy + SuperNova (folding sub-schemes), Lasso (a
  component, not an IVC framework), and Boojum (Goldilocks STARK, EraVM-specific) — the
  three systems the brief named that were missing.
- MIGRATION_PLONKY3_SPIKE_RESULT.md: add a top-of-file PARTIALLY-SUPERSEDED banner and
  correct escape-route #1 (the cross-layer capability was present all along via PR #407,
  not a missing upstream feature) — so a reader landing on the gate memo is pointed to the
  overturning result.
- probe_q: simplify the self-referential air_public_targets shape assertion.

* feat(plonky3): Probe R — carrier-table IVC chain threads a counter across 4 layers (GO)

A real depth-4 IVC chain: each layer is a prove_batch proof of a custom CarrierAir with
two public values [v_in, v_out] (AIR enforces v_out == v_in + 1, both bound to committed
trace cells); each IVC link verifies both adjacent carrier proofs in-circuit via
verify_batch_circuit (their public values surface as non-empty air_public_targets, not
[0,0,0]) and connects prev.v_out == cur.v_in. The counter is provably carried layer-0 ->
layer-3 (V_3 == V_0 + 3). Negatives: a wrong forwarded value is rejected (WitnessConflict
on the thread bind; a control with the bind removed accepts it, isolating the cause); a
carrier claiming an uncommitted public value is rejected (OodEvaluationMismatch). Does NOT
use build_and_prove_next_layer, so upstream #436 is not hit. Public API only, no fork.

This is the end-to-end empirical confirmation of Path 1+5: the cross-layer state IVC the
original NO-GO deemed impossible is buildable via custom public-value-emitting tables.

* docs(plonky3): gate memo banner — GO via Path 1+5 (carrier tables), Probe R confirms end-to-end

* test(plonky3): Probe R-cost — carrier chain per-transition cost at 2^16 inner scale (within budget)

* docs(plonky3): record Probe R-cost in gate memo — carrier chain within warm budget, add probe_q/r/r_cost rows

* docs(plonky3): review polish — gate probe_r_cost verdict on STARK-prove class (not witness-gen floor), tag superseded Gate-decision heading

* test(spike): add Probe S fair BabyBear vs Plonky2 prover bench

* docs(spike): review polish — honest S-box degree-3-vs-7 magnitude (~1.5-2.5x, verdict robust), bump test count 20->21 + Probe S table row

* test(spike): add Probe V degree-7 S-box bench on working HidingFriPcs recipe

* test(spike): add Probe W real HidingFriPcs vs blowup-2 zk-proxy delta

* docs(plonky3): add cutover playbook (Doc 1) + upstream maintenance plan (Doc 4)

* docs(plonky3): correct Probe S optimism with Probe V/W — degree-7 1.67x + true-hiding 3x (~5x combined), production config slower at 2^16, net verdict pending Probe T

* test(spike): add Probe T real-circuit Plonky3 prove-cost estimate

Cost-faithful representative workload for the real zkCoins state-transition
circuit under TRUE production crypto (degree-7 Poseidon2 + Keccak-hiding MMCS
+ HidingFriPcs, num_random_codewords=4). Models the real cost drivers (~4500
Poseidon2 hashes + ~50k non-hash gates) as a two-table batch, NOT the business
logic. Sweeps the non-hash table height over 2^13..2^16 to bracket the unknown
real layout.

Finding: real multi-table prove_batch (p3-batch-stark) WORKS with HidingFriPcs
+ mixed degree-7/degree-3 instances; verify_batch succeeds. At the realistic
layout (~2^13-2^14) Plonky3+BabyBear proves in ~312-449 ms warm p50 vs Plonky2
4350 ms = ~10-14x faster, ~2-3x lower RSS, near-zero circuit build (0.07 ms vs
8.2 s). Faster across the entire sweep including the 2^16 ceiling.

* docs(plonky3): add wire/storage format migration (Doc 2) + carrier-table crypto-audit spec (Doc 3)

* docs(plonky3): integrate Probe T — real circuit 10-14x faster under production crypto; V/W 2^16 was hash-saturation; full-prove verdict pending X+U

* test(spike): add Probes X (aggregator recursion overhead), Y (cold-start), Z (verifier), AA (sustained-load soak)

* docs(plonky3): Probe U e2e projection + integrate X/Y/Z/AA net verdict — send is wash/slower (recursion-dominated), mint ~2x, cold-start 38.7x, no leak

* docs(plonky3): full migration audit summary — honest mixed verdict, decisive X-prime lever, operator decisions

* docs(plonky3): redact internal host names from cutover playbook — role language only (review blocker)

* test(spike): Probe X' batched-aggregator lever — same-vk verifier amortization

Measure whether batching the 8 same-vk source proofs cuts the flat 8+1 aggregator cost Probe X reported (4.0s non-zk / 6.7s zk). Two framings, real STARK-prove via prove_all_tables: X'-a proves the 8 sources as one multi-instance BatchProof verified in-circuit once (lower bound: 0.98s non-zk / 1.66s zk, 4.1x reduction); X'-b proves 8 independent same-vk proofs as in the real protocol (3.97s non-zk / 6.69s zk, ~1.0x = flat). The recursion API verifies one BatchProof per verify_batch_circuit, so independent same-vk proofs cannot share the verifier — the batched floor is unreachable for /api/send. Realistic full send recomposes to 9.9s non-zk / 12.6s zk, a wash-or-loss vs Plonky2. Batching does not rescue the send case; MAX_IN_COINS reduction is the lever.

* docs(plonky3): resolve batching lever via Probe X-prime — not reachable in-protocol, send case rests on MAX_IN_COINS; test count 29

* docs(plonky3): update Fair-Performance lead to the resolved mixed verdict (T/X/X-prime/U)

* test(spike): Probe AB recursion-friendly levers — cheaper-inner-FRI 2.4x (64-bit), Poseidon2-MMCS already baseline, ZK-only-outer ~0

* test(spike): Probe AC MAX_IN_COINS sweep — aggregation ~linear in fan-in (~448ms/coin); N=4+cheaper-FRI cuts prove ~4x; e2e capped by node overhead

* test(spike): Probe AD KoalaBear-vs-BabyBear field comparison — split verdict; KoalaBear transition ~1.26x faster (degree-3 leaf S-box) but dominant 8+1 aggregation ~2.1x SLOWER (20 vs 13 partial rounds in recursion verifier); recommend STAY on BabyBear

* test(spike): probe AE — composed best-config full send-prove measurement

* docs(plonky3): recursion-reduction research (AB-AE) — send speed case recoverable: MAX_IN_COINS=4 alone 1.9x, +64-bit inner FRI 3.32x; KoalaBear ruled out; 33 tests

* docs(plonky3): apply resolutions — keep MAX_IN_COINS=8 (no UX regression), 64-bit inner FRI as port-phase auditor gate, port HOLD; recommended N=8+q48 = 2.25x send-prove

* chore: remove Plonky3 migration content from staging (#217)

The Plonky3 recursion spike, its migration write-ups, and its benchmark
results were merged to staging only (PRs #211/#212/#214) and must not be
promoted to develop. Remove them here so the next staging -> develop
auto-promote carries no Plonky3-migration artifacts. Everything removed is
archived verbatim in zk-coins/research.

- delete the plonky3-recursion-spike crate (36 files)
- delete MIGRATION_PLONKY3.md / _SOLUTIONS_RESEARCH / _SPIKE_RESULT
- delete docs/migration/PLONKY3_*.md (5 files)
- delete scripts/bench/results/plonky3-*.md (5 files)
- restore the workspace Cargo.toml to develop's form (drop the now-unused
  `exclude = ["spikes/plonky3-recursion-spike"]`)

* feat(api): GET /api/history/{id} — per-transaction detail endpoint (TxDetail) (#218)

The wallet's transaction-detail page needs more than the lean
/api/history list row. Add a scoped detail endpoint that returns
everything the node can derive for one account_history row without a
schema change:

- All HistoryItem core fields (txid/timestamp/direction/amount/status/
  block_height/...), via the same history_row_to_item mapping so the
  two endpoints cannot drift.
- The decoded account-state snapshot of the mutation: usable balance
  before/after (settled + coin_queue, mirroring balance_from_account_blob),
  the post-mutation num_sends (the wallet's authoritative BIP-32 child
  index), and the commitment public key (33-byte compressed hex).
- The verifier circuit digest (proof-system identity) from
  circuit_digest_meta; a read failure degrades the field to null.
- pending_inscriptions.commit_output_value when an inscription row
  exists (detail-only; the list query stays lean).

Scoping: the row must match (id, address) AND have a user-facing source
(mint/send/receive) — wrong-address or internal rows 404 identically,
so ids cannot be enumerated across accounts. Malformed address or a
non-integer/non-positive id is 422 (id parsed from the path as a string
so the read surface keeps one validation contract; axum 0.7 would
otherwise 400).

Tests: handler-level unit tests for every branch (422 x5, 404 x2,
500 x2 incl corrupt-blob, 200 happy + digest), db-level tests for the
scoped item query incl the inscription join, pure-fn tests for the
decoders, api_remote live round-trip (mint -> list -> detail) +
validation contract, openapi smoke (path + TxDetail schema).

* chore: move benchmark output to research (#219)

Per the project model the node repo carries code/build/standard files
only - no benchmark output. Delete scripts/bench/results/ (README +
m5-max HTTP-mint-sweep CSV, probe_r2 JSON, m5-max-vs-m3-ultra write-up).
The bench harness (node/src/bin/probe_r2.rs) stays; only the output
moves. Archived verbatim in zk-coins/research benchmarks/node-runtime/.

---------

Co-authored-by: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Sign up for free to 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.

1 participant