Skip to content

Constant-size proofs + stateless verification (v0.3.0)#50

Open
adamkrellenstein wants to merge 11 commits into
mainfrom
crypto/constant-size-stateless
Open

Constant-size proofs + stateless verification (v0.3.0)#50
adamkrellenstein wants to merge 11 commits into
mainfrom
crypto/constant-size-stateless

Conversation

@adamkrellenstein

@adamkrellenstein adamkrellenstein commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Reconciles the two open PoR PRs — #44 (constant-size proof) and #49 (stateless verify) — into one coherent model. Supersedes both.

Why one PR

#44 and #49 branched independently off main and conflicted at the design level, not just textually: #44 makes the proof constant-size by dropping challenge_ids/ledger_indices and moves the ledger to stable append-only slots; #49 (written against the old format) still read those fields and assumed lexicographic ordering. Those index models are mutually exclusive, so this lands them together.

The reconciled model

  • Proof is constant-size (wire v3): {compressed_snark, ledger_root, aggregated_tree_depth}. The challenge set is supplied out-of-band; the proof carries no per-file data, so its size is independent of challenge/file count.
  • plan.rs: make_plan takes AggInputs; per-file ledger indices are resolved from a LedgerView on both prove and verify paths (never read from the proof). AggInputs::Proof carries only root + depth.
  • verify.rs: LedgerView trait + registry-backed StatelessLedger { valid_roots, files: file_id → (slot, rc) } + verify_stateless, sharing verify_with with the ledger path.
  • ledger.rs: aggregate_root/aggregate_root_from_files take (rc, slot) / (root, depth, slot) and mirror rebuild_tree exactly (append-only slots, zero-filled gaps, sparsity guard, power-of-two padding) — byte-identical to FileLedger::root.
  • Bump to 0.3.0 (breaking wire + API). Carries the Soundness: non-strict to_bits_le on challenge/ledger index lets a prover open leaf idx+1 #46 strict-bit soundness fix from Constant size proof #44.

Design decisions

  • Drop the challenge_ids equality check (from Add stateless PoR verification (verify_stateless + aggregate_root) #49). Safe: the verifier rebuilds every SNARK public input from the supplied challenges (incl. block_height/prover_id/num_challenges via Challenge::id()initial_state), so a mismatched proof fails SNARK verification regardless. Confirmed by the audit below.
  • Append-only slots over lexicographic (from Constant size proof #44). Stable slots keep a file's index identical across historical roots, which is what makes cross-block aggregation and stateless verification work; lexicographic order shifts indices on every insert.
  • Stateless registry carries slots. StatelessLedger holds file_id → (slot, rc) (not just valid roots) so a contract keeping its file registry on-chain can resolve indices exactly as the live ledger does — enabling the host to drop its mutable in-memory FileLedger later.

Verification

  • Full suite 294 passed / 0 failed (47 binaries, all security suites incl. security_ledger, security_malicious_prover, stateless_verify). New stateless_verify.rs asserts stateless verify ≡ ledger verify (single + multi-file) and aggregate_rootFileLedger::root (incl. sparse slots).
  • Adversarial LLM soundness re-audit (the same kind that found Soundness: non-strict to_bits_le on challenge/ledger index lets a prover open leaf idx+1 #46): no high/medium findings across the challenge-binding, stateless-registry, cross-block-depth, aggregate_root, and single-file attack classes. One doc-hardening note added to StatelessLedger.
  • fmt + clippy clean; cargo audit passed (pre-push hook).

Supersedes


Note

High Risk
Changes proof serialization, ledger index semantics, and verification public-input construction in a security-critical PoR/SNARK stack; incorrect registry or root handling could break binding or cross-block validity.

Overview
Breaking release (0.3.0) that unifies constant-size proofs and stateless verification around append-only ledger slots instead of lexicographic ordering.

Proof wire format v3 drops serialized challenge_ids and ledger_indices; proofs carry only compressed_snark, ledger_root, and aggregated_tree_depth. Verifiers must supply the exact Challenge set out-of-band; binding comes from canonical challenge ordering, initial_state, and SNARK public inputs—not from fields inside the proof bytes.

Prove/verify planning uses Plan::make_plan(challenges, AggInputs) so aggregation root/depth come from the live ledger (prove) or the proof (verify), while per-file ledger indices are always resolved via LedgerView on both paths. New verify_stateless + StatelessLedger (valid_roots + file_id → (slot, rc)) share verify_with with the live FileLedger path.

FileLedger assigns stable indices on add_file; add_files requires explicit ledger_index for reconstruction and rejects duplicate file_ids. The aggregated tree is built by slot (zero-filled gaps, sparsity cap). Helpers aggregate_root, aggregate_root_from_files, and incremental LedgerFrontier match FileLedger::root for on-chain-style registries. Ledger persistence bumps to format v2.

Circuit hardening (#46): Merkle path selection uses to_bits_le_strict for challenge and ledger-index decompositions.

Docs/tests/Picus constraint counts updated for the new model; PorSystem::verify no longer compares proof.challenge_ids to caller challenges.

Reviewed by Cursor Bugbot for commit 137bb9e. Bugbot is set up for automated code reviews on this repo. Configure here.


Closes #46 (strict bit-decomposition on both index sites).
Issue #19 (incremental tree-update) is addressed by the LedgerFrontier added here.

@codspeed-hq

codspeed-hq Bot commented Jun 16, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 13 untouched benchmarks
⏩ 13 skipped benchmarks1


Comparing crypto/constant-size-stateless (137bb9e) with main (fe9f977)

Open in CodSpeed

Footnotes

  1. 13 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

adamkrellenstein and others added 11 commits June 18, 2026 14:08
…file-merkle-path); fixes Component + Monolithic matrix Errors
Carries the issue #46 soundness fix into the constant-size circuit. The Merkle
path-selection indices were derived with non-strict `to_bits_le`, which only
enforces Σ bitᵢ·2ⁱ ≡ x (mod p), not canonicity. Since the Pallas modulus is just
above 2^254 (NUM_BITS=255, p ≡ 1 mod 2^32), a prover can witness the (x + p)
decomposition whose low bits encode index+1 and open a leaf other than the
challenged one (file tree) or steer the aggregation path off the public index.

Switches the four index-derivation sites — challenge_with_idx and
ledger_index_public in both synth.rs and formal_components.rs — to
to_bits_le_strict. Completeness preserved; the idx+1 opening is closed.

Verified: build + api_functionality, depth_zero_cheat_regression,
security_malicious_prover, security_ledger all pass.
…e-identical to aggregate_root; removal out of scope)
Unify the constant-size proof work (wire v3: Proof carries only
compressed_snark + ledger_root + aggregated_tree_depth) with stateless
verification on one model.

- plan.rs: make_plan takes AggInputs; per-file ledger indices are always
  resolved from a LedgerView (live FileLedger or stateless registry), never
  read from the proof. AggInputs::Proof drops ledger_indices.
- verify.rs: LedgerView trait + registry-backed StatelessLedger
  {valid_roots, files: file_id -> (slot, rc)} + verify_stateless, sharing
  verify_with. The challenge_ids equality check is removed: public inputs
  are rebuilt from the supplied challenges, so a mismatched proof fails the
  SNARK regardless.
- ledger.rs: aggregate_root / aggregate_root_from_files take (rc, slot)
  pairs and mirror rebuild_tree (append-only slots, zero-filled gaps,
  sparsity guard, power-of-two padding), byte-identical to FileLedger::root.

Bump to 0.3.0 (breaking wire + API). Full suite green (294 passed);
stateless verify matches ledger verify for single- and multi-file proofs.
@adamkrellenstein adamkrellenstein force-pushed the crypto/constant-size-stateless branch from b83944f to 137bb9e Compare June 18, 2026 12:48
@adamkrellenstein adamkrellenstein requested review from wilfreddenton and removed request for cryptograph and wilfreddenton June 18, 2026 12:48
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.

Soundness: non-strict to_bits_le on challenge/ledger index lets a prover open leaf idx+1

3 participants