Constant-size proofs + stateless verification (v0.3.0)#50
Constant-size proofs + stateless verification (v0.3.0)#50adamkrellenstein wants to merge 11 commits into
Conversation
Merging this PR will not alter performance🎉 Hooray!
|
…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.
Crypto/incremental root perf
b83944f to
137bb9e
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c35891f. Configure here.
| "Failed to deserialize ledger: {}; legacy v2 load failed: {}", | ||
| current_err, legacy_err | ||
| )) | ||
| })?, |
There was a problem hiding this comment.
Missing v1 ledger load path
High Severity
FileLedger::load only accepts wire format version 3 or a v2 legacy shape with indexed entries. Prior releases persisted ledgers as format version 1 with unindexed FileLedgerEntry values, and there is no v1 loader or migration. Upgrading from 0.2.x leaves existing on-disk ledgers unloadable.
Reviewed by Cursor Bugbot for commit c35891f. Configure here.
c35891f to
137bb9e
Compare


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
mainand conflicted at the design level, not just textually: #44 makes the proof constant-size by droppingchallenge_ids/ledger_indicesand 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
Proofis 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_plantakesAggInputs; per-file ledger indices are resolved from aLedgerViewon both prove and verify paths (never read from the proof).AggInputs::Proofcarries only root + depth.verify.rs:LedgerViewtrait + registry-backedStatelessLedger { valid_roots, files: file_id → (slot, rc) }+verify_stateless, sharingverify_withwith the ledger path.ledger.rs:aggregate_root/aggregate_root_from_filestake(rc, slot)/(root, depth, slot)and mirrorrebuild_treeexactly (append-only slots, zero-filled gaps, sparsity guard, power-of-two padding) — byte-identical toFileLedger::root.Design decisions
challenge_idsequality 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_challengesviaChallenge::id()→initial_state), so a mismatched proof fails SNARK verification regardless. Confirmed by the audit below.StatelessLedgerholdsfile_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-memoryFileLedgerlater.Verification
security_ledger,security_malicious_prover,stateless_verify). Newstateless_verify.rsasserts stateless verify ≡ ledger verify (single + multi-file) andaggregate_root≡FileLedger::root(incl. sparse slots).aggregate_root, and single-file attack classes. One doc-hardening note added toStatelessLedger.cargo auditpassed (pre-push hook).Supersedes
Note
High Risk
Changes proof serialization, verifier trust assumptions (out-of-band challenges + caller-supplied stateless registry/roots), ledger index semantics, and circuit constraints—core soundness and cross-network compatibility surfaces.
Overview
Breaking release (0.3.0) that reconciles constant-size proofs and stateless verification around one ledger model: append-only stable slots instead of lexicographic
file_idordering.Proof wire format v3 —
Proofnow serializes onlycompressed_snark,ledger_root, andaggregated_tree_depth(nochallenge_idsorledger_indices). Callers pass the intendedChallengeset out-of-band;PorSystem::verifyno longer compares proof-embedded challenge IDs. Binding comes from canonical challenge sorting,initial_statederived fromChallenge::id(), and verifier-rebuilt public inputs.Prove/verify planning —
Plan::make_plantakesAggInputs(ledger root from liveFileLedgeron prove, from the proof on verify). Per-file ledger indices are always resolved throughLedgerView::lookup, never read from the proof.Stateless path —
LedgerView, registry snapshotStatelessLedger(valid_roots+file_id → (slot, rc)), sharedverify_with, and publicverify_stateless.aggregate_root/aggregate_root_from_filesandLedgerFrontier(incremental O(log n) root updates) matchFileLedger::rootbyte-for-byte for contiguous append-only slots.Ledger semantics —
IndexedFileLedgerEntry, immutablefile_id(duplicates rejected),add_filesrequires explicit indices for reconstruction; aggregated tree built byledger_indexwith zero-filled gaps and a sparsity cap. Ledger on-disk format bumps to v2.Circuit (#46) — Merkle and aggregation path selection use
to_bits_le_strictinstead ofto_bits_leso non-canonical field decompositions cannot steer paths.Docs (
PROTOCOL.md,SECURITY.md), security/integration tests, and Picus expected constraint counts are updated for the new model.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.