Skip to content

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

Merged
TaprootFreak merged 1 commit into
stagingfrom
feature/tx-detail-endpoint
Jun 7, 2026
Merged

feat(api): GET /api/history/{id} — per-transaction detail endpoint (TxDetail)#218
TaprootFreak merged 1 commit into
stagingfrom
feature/tx-detail-endpoint

Conversation

@TaprootFreak

@TaprootFreak TaprootFreak commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

What & why

The wallet's upcoming transaction-detail page needs more than the lean /api/history list row. This adds a dedicated, scoped detail endpoint — server is the decision-authority for what data exists, so the detail surface is built node-side first.

GET /api/history/{id}?address=<hex>TxDetail: all HistoryItem core fields plus everything else the node can derive for one account_history row without a schema change.

Scope decision (heuristic: professional + grounded in real data)

Surfaced because it is always available or a cheap join:

Group Fields
Core (mirrors HistoryItem) id, txid, timestamp, direction, amount, counterparty, status, block_height, memo
Decoded account-state snapshot balance_after, balance_before (null on first row), num_sends_after, commitment_public_key (33-byte compressed hex)
Proof / verification circuit_digest (verifier circuit identity from circuit_digest_meta)
On-chain commit_output_value (from pending_inscriptions when an inscription exists)

Deliberately NOT added (would be speculative nulls or need new machinery): per-tx reveal_txid/fee (requires Bitcoin-tx hashing + the publisher triggering_commit_txid GUC, unset today — txid/block_height/commit_output_value already light up automatically once it is) and counterparty (no schema column — same null as the list). The honesty contract matches HistoryItem: fields the schema can't populate stay null, never fabricated.

Scoping / safety

The row must match both id and address and be user-facing (mint/send/receive). A wrong-address lookup or an internal scanner/recovery row returns 404, identical to a missing row — ids cannot be enumerated across accounts. Malformed address or a non-integer/non-positive id422 (id parsed from the path as a string so the read surface keeps one validation contract).

Reuse

tx_detail_from_row calls the existing history_row_to_item for the shared core, so the list and detail endpoints can never disagree on direction/amount/status/txid. The list query stays lean (no commit_output_value / no blob re-decode); only the detail path pays for the extra column + decode.

Tests

  • Handler unit tests (dead-pool + live postgres:17 testcontainer) for every branch: 422 ×5 (missing/empty/bad-hex address, non-integer + non-positive id), 404 ×2 (unknown id, wrong-address scoping), 500 ×2 (DB error, corrupt blob), 200 happy-path (decoded snapshot) + circuit-digest-present.
  • db tests for the scoped item query incl the pending_inscriptions join + IDOR scoping + internal-source filter.
  • Pure-fn tests for account_meta_from_blob / tx_detail_from_row.
  • api_remote live round-trip (mint → list → detail) + the validation/scoping contract.
  • openapi smoke: /api/history/{id} path + TxDetail schema registered.

Local reproduction (M5, node against public Mutinynet)

Built + booted the node, minted a fresh address, and fetched the detail:

GET /api/history/119?address=f14eac…780b
{
  "id": 119, "direction": "mint", "amount": 10000, "status": "pending",
  "balance_after": 10000, "balance_before": null, "num_sends_after": 0,
  "commitment_public_key": null,
  "circuit_digest": "e05c08f46fa5cd2a7cfaed91a3d404f6d0e87c7c49dea2ee9aaf7baf71ecd47b",
  "txid": null, "block_height": null, "commit_output_value": null
}

cargo fmt --check, both CI clippy … -D warnings invocations, the full node lib test suite, and the api_remote history tests (7/7) all pass locally; the 100% line+function coverage gate was run locally as well.

Consumed by the SDK (getTransaction) and the app's /tx/[id] detail page in follow-up PRs.

Base branch

Targets staging per the repo flow (feature → staging → develop → main; Promote #215, Release #197). The branch is rebased onto staging; the full local gate (fmt, both CI clippy invocations, the touched lib tests incl. live-Postgres handler tests) was re-run green on the rebased base.

@TaprootFreak
TaprootFreak marked this pull request as ready for review June 7, 2026 10:29
@TaprootFreak TaprootFreak added the ci:full Trigger heavy CI jobs (Server + Shared Tests + Coverage Gate, ~60-90 min on M3 Ultra) label Jun 7, 2026
…xDetail)

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).
@TaprootFreak
TaprootFreak force-pushed the feature/tx-detail-endpoint branch from fcbd207 to 0e0bfd0 Compare June 7, 2026 11:10
@TaprootFreak
TaprootFreak changed the base branch from develop to staging June 7, 2026 11:10
@TaprootFreak
TaprootFreak merged commit 12fa65c into staging Jun 7, 2026
4 of 5 checks passed
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

ci:full Trigger heavy CI jobs (Server + Shared Tests + Coverage Gate, ~60-90 min on M3 Ultra)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant