mirror-pool: Rust-only ZK behavioral-privacy protocol — circuit, native program, verifiable ceremony, compliance, live devnet - #3
Conversation
Establish the Cargo workspace and the crypto foundation the rest of the
protocol builds on.
- Five-crate workspace (common, circuit, program, relayer, cli), resolver 2,
arkworks 0.5 pinned once in [workspace.dependencies].
- common: the single source of truth for crypto constants.
- poseidon: circom-compatible BN254 x5 permutation hand-rolled over
light-poseidon's pinned constants; digest read from state[0] (NOT arkworks
PoseidonSponge's state[capacity], which would silently diverge). Equality
test asserts it matches light-poseidon — the impl behind sol_poseidon — over
1000 random inputs per arity.
- field: canonical big-endian 32-byte encoding matching the on-chain hasher
and groth16-solana; rejects non-canonical encodings rather than reducing.
- shared TREE_DEPTH (20) and ROOT_HISTORY_SIZE (64).
- program: minimal native solana-program entrypoint so the build-sbf path is
exercised in CI from day one (validated: solana-program 2.3 builds under
platform-tools v1.54). No unwrap/expect/panic in the handler.
- circuit/relayer/cli: compiling skeletons with real dependency sets; bodies
land in later milestones and fail loudly until then.
- CI: fmt + clippy (-D warnings) + test on host, plus a build-sbf job.
- README + ARCHITECTURE with the Poseidon consistency invariant and threat-model
summary; milestone status table.
Test evidence: `cargo test --workspace` green (8 tests incl. the Poseidon
equality test); fmt/clippy clean; `cargo build-sbf` produces program bytecode.
Implements the zero-knowledge membership statement and a full prove/verify pipeline, all in Rust (arkworks, no Circom). - common::merkle: off-chain reference Merkle tree (fixed depth, Poseidon two-to-one) producing the path witness; hashes via common::poseidon so it matches the circuit and chain. Reference for M4's on-chain incremental tree. - circuit::poseidon_gadget: the circom permutation as R1CS, built from the SAME pinned constants common re-exports. Reads the digest from state[0] to match the native/chain hasher. Tested equal to common::poseidon. - circuit::circuit::MembershipCircuit: commitment + Merkle inclusion, epoch-scoped nullifier, and an action_binding pinned into the R1CS (squaring constraint) so proofs can't be replayed for a different action. - circuit::prover: circuit-specific Groth16 setup, prove, verify, and the canonical PublicInputs encoding (big-endian, order = root, nullifier, epoch, action_binding) shared with the on-chain verifier. build_witness recomputes root and nullifier from the secret + path so they can't drift. Test evidence: `cargo test -p mirror-pool-circuit` green — 10 tests. Real Groth16 setup/prove/verify on the happy path plus all mandatory negative cases (wrong secret, tampered path -> unsatisfiable, action-binding mismatch, wrong epoch), and gadget==native Poseidon over random inputs. fmt/clippy clean. Note: the trusted setup is local (dev/test); production keys must come from a multi-party ceremony. Documented in prover::setup and ARCHITECTURE.md.
Prove that a real arkworks proof verifies on-chain via groth16-solana, under the compute budget. This is the gate; nothing builds on top until it is green. - circuit::solana: convert arkworks VK/proof to the on-chain byte layout, handling the three documented quirks — big-endian limbs, G2 c1||c0 ordering, and proof_a negation. Host test runs a real proof through groth16-solana's verifier and rejects a tampered public input. - program::verifier: byte-only on-chain verification (never links arkworks, so it stays within the compute budget). Parses the flat VK layout with bounds checks; verify_membership returns typed errors and never treats an unverifiable proof as valid. - program: VerifyMembership instruction + dispatcher (no unwrap/expect/panic). - program tests/verify_host: real proof verifies through the exact groth16-solana code the program runs; tampered proof/public-input/VK rejected. - bench/ (workspace-EXCLUDED): litesvm CU benchmark running the actual SBF bytecode over a real proof. Kept out of the workspace so litesvm can track a newer Solana release without forcing the program off solana-program 2.3; it crosses the boundary via the .so and a fixture file only. - CI build-sbf job now also generates the fixture and runs the benchmark. Measured: VerifyMembership consumes ~98k compute units, well under the ~200k budget. Test evidence: `cargo test --workspace` green (28 tests + 1 ignored fixture gen); `cargo run --manifest-path bench/Cargo.toml` prints 98,179 CU and PASS. fmt/clippy clean on workspace and bench.
Add the pool state machine's tree half: initialize a pool and deposit commitments, advancing an incremental Merkle tree with a root-history buffer. - common: split crypto behind a default `crypto` feature so the program can depend on `common` for the pure protocol constants (TREE_DEPTH, ROOT_HISTORY_SIZE) WITHOUT compiling arkworks/light-poseidon into the BPF binary (light-poseidon's param builder overflows the BPF stack frame). - program::state::PoolConfig: zero-copy bytemuck Pod cast directly out of the account buffer and mutated in place — the 3.4 KB struct never touches the 4 KB BPF stack. u64 counters stored as LE bytes to keep align = 1. Incremental insert (filled subtrees), root-history ring, is_known_root. - program::merkle: two-to-one hash via the sol_poseidon syscall (solana-poseidon), plus zero-hash computation. - Instructions InitializePool (creates PDA ["pool", authority]) and Deposit; borsh-encoded, VerifyMembership kept as variant 0. No unwrap/expect/panic. - Host tests: incremental root == off-chain reference at every step; on-chain Poseidon == common::poseidon (ties the program to the circuit hasher); history eviction; tree-full rejection; account length exact. - bench e2e binary: runs the real SBF bytecode in litesvm through InitializePool + 3 Deposits and confirms the on-chain root matches the reference tree. CI runs both bench binaries. Measured: initialize ≈24k CU, deposit ≈18.5k CU. Test evidence: `cargo test --workspace` green; e2e prints "on-chain root matches reference"; build-sbf clean (no stack-frame warnings); fmt/clippy clean.
The protocol's core: verify a membership proof and, if valid and unused this epoch, execute an action signed by the pool PDA — the member is never the actor. - program::action: the `Action` trait (extensibility seam) + `NoOpAction` (a self-CPI proving the pool PDA signs) + `dispatch`. action_binding = Poseidon(selector), mirrored by common::poseidon::action_binding, so a proof authorizes exactly one action. - ExecuteAction handler enforces, in order: epoch match, known root, action binding, Groth16 verification, nullifier-unused; then creates the nullifier marker PDA (["nullifier", hash] — existence = spent) and CPIs the action signed by the pool PDA. Fee payer is the relayer, never the member. - PoolConfig gains current_epoch and the embedded verifying_key (set at InitializePool); still zero-copy, VK parsed in place. - New typed errors: UnknownRoot, EpochMismatch, ActionBindingMismatch, NullifierAlreadyUsed, UnknownAction, UnauthorizedActor, etc. - bench `flow` binary runs the FULL path on real SBF bytecode with genuine proofs: init → 4 deposits → prove → execute_action (PDA CPI) → and rejects replay (Custom 16), action-binding mismatch (15), tampered proof (3), and wrong epoch (14). CI runs it. Measured CU: initialize ≈24k, deposit ≈18.6k, execute_action ≈110k (verification ~98k + tree/nullifier/CPI). Test evidence: `cargo test --workspace` green; flow binary prints all four negatives correctly rejected; build-sbf clean; fmt/clippy clean (ws + bench).
Complete the pool state machine and the off-chain relay path. - Epochs: open_epoch/close_epoch cranks (authority-only). execute_action now requires an open epoch AND epoch_id == current_epoch, forcing actions to cluster in a shared window (the anonymity set is per-window). Nullifiers stay epoch-scoped. - Action binding now covers parameters: action_binding = Poseidon(selector, params_digest), params_digest = sha256(params) with the top byte cleared (canonical field element, syscall-computable both sides). A proof authorizes a specific action AND its params (amount, recipient). - TransferAction: a real integration — the pool PDA disburses SOL to a bound recipient on a member's behalf (direct lamport move, since System transfer can't debit a program-owned account; pool kept rent-exempt). The Action trait extension guide is in ARCHITECTURE.md. - relayer crate (lib + bin): builds and submits execute_action as the sole fee payer so the member's wallet never appears (SPEC §4.4). RelayJob is the self-contained hand-off; `relay` submits one, `batch` submits a directory within a window (epoch batcher) and reports the anonymity-set size. Stays on the solana-2.3 line; depends on the program only for borsh encoding + seeds. - bench `flow` extended: init → deposit → open epoch → no-op action → real transfer (recipient receives 750M lamports from the pool PDA) → and rejects replay(16), binding mismatch(15), tampered proof(3), wrong epoch(14), and epoch-not-active(21). Measured CU: open/close ≈1.5k, execute_action ≈110k. Test evidence: `cargo test --workspace` green; flow binary passes all steps and negatives; build-sbf clean; fmt/clippy clean (ws + bench + relayer).
The first-class compliance differentiator: selective disclosure to a designated auditor, and a pluggable entry screen — framed as compliant behavioral privacy, not evasion. - common::compliance: viewing-key selective disclosure. seal_disclosure seals a member's secret to an auditor's X25519 key (ECIES + ChaCha20-Poly1305); open_disclosure recovers it; verify_disclosure attributes an on-chain nullifier for a given epoch. Per-member/per-auditor — no master key, no way to enumerate non-disclosing members. - program: RegisterViewingKey stores a DisclosureRecord at ["viewing", commitment] (commitment, auditor, sealed secret). Small borsh record. - program: deposit-screening hook. PoolConfig.screening_authority (all-zero = off by default); SetScreeningAuthority (authority-only) toggles it; when on, deposit must be co-signed by the authority (ScreeningRequired otherwise). Pluggable: point it at an allowlist/attestation program's authority. - bench `compliance` binary: on real bytecode, unscreened deposit rejected (Custom 24), screened accepted, disclosure registered + opened by the auditor (attributes the nullifier), stranger cannot open. CI runs it. Test evidence: `cargo test --workspace` green (common compliance unit tests + existing); compliance binary PASS; build-sbf clean; fmt/clippy clean.
Complete the developer surface and documentation. - cli (`mirror-pool`): setup (dev Groth16 trusted setup → proving/verifying keys + on-chain VK bytes), keygen (member secret or auditor viewing keypair), prove (build a membership proof → relay job), disclose (seal secret to an auditor), sim (simulate N members over epochs, report the anonymity set and warn on timing-vulnerable single-action windows), and deposit/execute (on-chain via RPC; execute goes through a relayer so the member never pays). Offline commands need no network and are exercised directly. - relayer: RelayJob slimmed — the action's trailing accounts are derived from the selector/params, so the job carries no member-linkable account data. - demo.sh: runs the entire protocol on an in-process local SVM (litesvm) against the real SBF bytecode — build, CU benchmark, full flow (init → deposit → epoch → no-op + real transfer → negatives), compliance, and the CLI. Reproducible, no external validator. - ARCHITECTURE.md: full, honest threat model — fee-payer linkage, single-action windows, timing correlation, amount/dust correlation, anonymity-set size, trusted setup, screening — with mitigations, residual leakage, and an explicit "does NOT defend against" list. README quickstart + devnet deploy path. Test evidence: `cargo test --workspace` green (38 tests); `./demo.sh` runs the full protocol end-to-end and prints PASS for every stage; fmt/clippy clean across the workspace and bench.
…IDATION.md) Work through VALIDATION.md's confidence gradient with dedicated, reproducible gates rather than confirmatory tests. L2 (soundness): - circuit::solana::offchain_and_onchain_verifiers_agree — the SAME proof verifies via arkworks Groth16::verify AND groth16-solana, and both reject the same tampered input (catches endianness/serialization drift). - flow: added the stale/unknown-root negative (Custom 13 UnknownRoot) — the one mandatory negative not previously exercised at the program level. - confirmed no mocked/stubbed crypto and no unwrap/expect/panic in non-test src. L3 (privacy red-team): new bench `trace` binary runs a multi-member epoch through a dedicated relayer and attacks the public trace — asserts the fee payer is always the relayer (never a member), no member is a signer, no account meta/PDA seed is member-derived, and reports the effective anonymity set (>1). L4 (deployability): flow asserts execute_action CU < 200k; the relayer now prepends ComputeBudgetInstruction::set_compute_unit_limit so the tx explicitly requests its budget. Devnet deploy is the one gate deferred to the operator (no funded keypair here); the litesvm gates run the identical .so. L5 (robustness): program/tests/robustness.rs fuzzes Instruction::unpack and ParsedVerifyingKey::parse (thousands of random inputs) — never panics, always a typed error; flow adds re-initialization (Custom 6) and unauthorized-crank (Custom 23) negatives. External review: Superteam BR's auditor-skill was unavailable in this environment; ran an independent audit of the on-chain handlers instead — no high-confidence exploitable vulnerability; two out-of-scope observations documented (global nullifier namespace = cross-pool DoS only; VerifyMembership benchmark reads an unchecked but side-effect-free VK). Honesty gate: ARCHITECTURE.md now states the assurance status (no formal audit, no formal circuit verification, dev-only trusted setup, anonymity-set minimum enforced operationally). VALIDATION-RESULTS.md maps every gate to its enforcing test/binary. CI runs the trace red-team.
…-chain min-k, denominations (Section 1)
1a. Nullifier PDA seed is now ["nullifier", pool, hash] (was ["nullifier",
hash]) so nullifiers can't collide or be griefed across pools. flow proves
the same nullifier hash is spent independently in two pools.
1b. VerifyMembership is now behind #[cfg(feature = "bench")] and declared LAST,
so production discriminants (0..=7) are unchanged and the DEPLOYED build-sbf
artifact does not contain it (it read an unchecked VK account and was pure
benchmark surface). robustness test verify_membership_absent_from_default_build
asserts tag 8 does not decode without the feature. The cu-bench uses a
separate --features bench build.
1c. On-chain minimum anonymity set. PoolConfig gains k_min (init param) and
epoch_actions (reset each open_epoch). execute_action rejects with
AnonymitySetTooSmall unless the on-chain lower bound
`members_deposited - actions_this_epoch >= k_min`. This is a conservative,
provable floor (documented). flow: rejected at set=1<2, accepted at 2>=2.
1d. Amount privacy. TransferAction only moves fixed DENOMINATIONS (0.1/1/10
SOL); the amount is proof-bound, so it is not a distinguishing feature.
Non-denominated amounts are rejected (InvalidDenomination). flow uses 1 SOL.
Also: removed the stale M4 `e2e` bench bin (superseded by flow + tree_logic);
updated relayer/CLI/bench nullifier derivation; new error codes 25/26; CI now
tests default (deployed) features and runs the bench build separately for the CU
benchmark; demo.sh rebuilds the deployed (bench-free) artifact after the CU step.
Evidence: flow enforces min-k, denomination, pool-scoped nullifier + all prior
negatives; cu-bench 98,629 CU on the bench build; trace/compliance green;
`cargo test --workspace` (default) green; fmt/clippy clean (ws + bench).
…ommands, professionalization (Sections 2,3,5) Section 2 (trusted setup): - circuit: dev_setup() — deterministic fixed-seed Groth16 setup; commit the resulting on-chain VK at setup/verifying_key.solana.bin and gate it with the setup_reproducible test (regenerate + byte-diff). CLI `setup` now uses it and warns loudly that it is dev-only. setup/README documents the command. - Honest note: arkworks' Groth16 does a self-contained setup and does not ingest an external powers-of-tau; the production multi-party Phase-2 ceremony path is written in SECURITY.md, and the shipped key is explicitly dev-only. Section 3 (security review): - SECURITY.md: threat-model pointer, trusted-setup status + ceremony path, known limitations, the review checklist, and findings/resolutions. auditor-skill was unavailable; an independent structured review was run instead (documented as self-review, not a third-party audit). Base + hardening reviews found no exploitable bug; the k_min-vs-Sybil limitation is documented, not overclaimed. Section 5 (professionalization): - CLI gains init-pool and crank (open/close) so the full flow is drivable over RPC against a deployed program. - README: CI badge, mermaid architecture diagram, Custom(n) error-code table, "adding an action" guide, deploy/devnet status, security + reproducible-build sections. - ARCHITECTURE: anonymity-set metric definition + enforced on-chain invariant + Sybil limitation; threat-table rows updated for k_min/denominations/Sybil. - #![deny(missing_docs)] on the common and circuit library crates (+ docs). - demo.sh and CI build the deployable artifact with --arch v3. Gates: fmt/clippy (all-features) clean; `cargo test --workspace` green incl. the setup reproducibility test.
Re-ran every VALIDATION.md gate. Records the newly-closed gates (pool-scoped nullifiers, benchmark-instruction removal, on-chain k_min, denominations, trusted-setup reproducibility) and the deployability status: the --arch v3 artifact deploys to a real Agave validator and the full flow runs against the deployed program over RPC via the CLI; live devnet remains deferred pending faucet funding. Honesty gate reaffirmed, including the k_min-vs-Sybil limitation.
…nonymity Trilemma framing (Addendum A, B) A — new `anonymity` crate (pure Rust, off-chain measurement, never an on-chain guarantee): - min_entropy_effective_k = 1/max_i p_i (single-guess adversary; Serjantov– Danezis PET'02, Díaz et al. PET'02, Smith FoSSaCS'09). - Reported per denomination×action-type bucket (worst bucket), over the association set AND over all deposits — the delta is the Sybil exposure the hardening pass documented, now quantified. Plus a dominance-adjusted figure (k − max-funder notes; Sweeney/l-diversity/t-closeness). - Wired into `cli sim` with a `--sybils` knob: e.g. 8 honest + 40 sybils → effective-k 8 over the association set vs 48 over all (gap 40). B — ARCHITECTURE now justifies the synchronized-epoch design with the Anonymity Trilemma (Das et al. S&P'18 + PoPETs'20): epochs spend latency to buy anonymity at low bandwidth; the operating point and its honest limits are stated. Docs: ARCHITECTURE gains the effective-k measurement section, the trilemma section, and a References section citing the primary papers. The measurement/ on-chain separation and the Sybil disclosure are preserved and deepened, not softened. Gates: anonymity unit tests green (incl. the Sybil-gap and dominance cases); fmt/clippy clean.
…eference; differentiators (Addendum C, D) C — circuit::association (Privacy-Pools-style, Buterin et al. 2023): - AssociationSet: a Merkle tree of approved commitments; prove_inclusion reuses the membership circuit against the set root (real ZK inclusion, no new circuit), bound to the action via the shared nullifier_hash; verify_inclusion rejects a proof against the wrong root. Tests: an approved member proves inclusion, an outsider cannot, wrong-root is rejected. - SanctionedSet: an OFF-CHAIN native exclusion reference (sorted-set adjacency witness) for the ASP flow. ZK on-chain non-membership is documented as future work — NOT claimed as implemented. - CLI `associate` produces and self-verifies an inclusion proof. - Exact guarantee documented (membership/non-membership only); on-chain enforcement of inclusion in execute_action is designed but off by default. Association sets are framed as the precondition for the effective-k metric and the narrowing (not elimination) of the Sybil gap. D — README "Design differentiators & eligibility" (Rust-only, native solana-program, compliance dimension, grounded metric, real-validator deploy). Devnet deploy remains deferred (faucet rate-limited); address recorded, steps in README/VALIDATION-RESULTS. Docs: ARCHITECTURE association-sets section + References; SECURITY known-limits updated (exclusion ZK is future work; ASP corruption re-introduces the Sybil gap). demo.sh shows the effective-k Sybil gap and a compliance inclusion proof. Gates: fmt/clippy(all-features + bench) clean; `cargo test --workspace` green incl. association inclusion/exclusion tests.
…ation layer) Adds the Addendum-v2 gate table (min-entropy effective-k per bucket over the association set vs all deposits with the Sybil-gap delta, dominance adjustment, Trilemma framing, ZK inclusion proof, off-chain exclusion reference, citations) and updates the L3 metric line. Honesty scope reaffirmed: effective-k is a measurement not an on-chain guarantee; association narrows but does not eliminate the Sybil gap; exclusion is an off-chain reference, not a ZK on-chain proof.
… (Final pass 1–2) Part 1 — trusted setup upgraded from a single-party public-seed dev key to a real multi-contributor Phase-2 MPC (arkworks-native, circuit::ceremony): - Each contribution re-randomizes the delta trapdoor with fresh entropy (delta_g1/g2 *= s; l_query/h_query *= s^-1), publishes a Schnorr PoK of s, and the pairing same-ratio check ties the g1/g2 updates to one s. The key is secure if >=1 contributor discarded their randomness. - Correctness gate: ceremony_key_still_proves_and_verifies runs a full prove+verify with the multi-contributed key (a wrong delta update would fail); tampered contributions are rejected; transcript round-trips. - Committed a real 3-contribution ceremony under setup/ (verifying_key.bin, verifying_key.solana.bin, transcript/transcript.bin). The proving key is not committed (large; operators run their own). - REPLACED setup_reproducible (regenerate-from-seed) with trusted_setup: verify the contribution chain, pin the transcript by SHA-256, and confirm the committed on-chain VK is the ceremony output. dev_setup / DEV_SETUP_SEED removed; CLI `setup` now runs a ceremony (--contributions), init-pool takes --verifying-key, associate takes --proving-key. SECURITY.md/setup README state the new "secure if >=1 honest contributor" assurance + single-operator and Phase-1 caveats. No honest limitation removed. Part 2 — precision fixes: - anonymity docs now state the adversary explicitly: uniform_effective_k is the EXTERNAL adversary (max p=1/k); dominance_adjusted is the DOMINANT-FUNDER adversary (max p=1/(k-m)), the honest worst case. - association inclusion enforcement status made unambiguous: verified off-chain (ASP-side); on-chain enforcement is a design note, NOT feature-gated code. Gates: ceremony + trusted_setup + association tests green; fmt/clippy (all-features + bench) clean.
…EC/VALIDATION
Part 3 of the Final Pass — a documentation overhaul with no functional code
change (source edits are comment/doc-string reference fixes only).
- Migrate SECURITY.md -> docs/security.md and rewrite its trusted-setup section
for the real multi-contributor Phase-2 ceremony (delta re-randomization,
Schnorr PoK, transcript pinned by hash, verified by circuit/tests/
trusted_setup.rs). Top-line caveat now reads "secure if >=1 contributor was
honest, but single-operator + no external Phase-1 -> testnet-grade" — the
forgeable public-seed dev language is gone.
- Add docs/{anonymity,compliance,circuit,deployment,testing}.md as focused
deep-dives; deployment.md records the live devnet program id
4YrUSMP2gG9v9SJAgQPNYpzvUSxqWVBBQwdc7g52xYPe, deploy signature, and explorer
link, and notes --arch v3 belongs to `cargo build-sbf`.
- Migrate VALIDATION-RESULTS.md -> docs/testing.md; flip devnet + trusted-setup
gates from deferred/reproducible to live/ceremony; refresh the honesty gate.
- Rewrite README.md as the front door: documentation index, live-devnet
differentiator with program id, ceremony language for `cli setup`, and a
Security section pointing at docs/security.md.
- git rm SPEC.md VALIDATION.md; repoint stale cross-links (ARCHITECTURE.md
threat-model anchor, source comments) to docs/.
- Fix a pre-existing clippy doc_lazy_continuation warning in the anonymity
crate so CI stays clean under -D warnings.
Verified: cargo fmt --check clean; cargo clippy --workspace --all-targets
--all-features clean under RUSTFLAGS=-D warnings; cargo test --workspace
--all-features green (trusted_setup + 12 circuit/ceremony tests included);
./demo.sh runs all six stages end-to-end (VerifyMembership 98,627 CU).
…tures Capture the deployed devnet program (4YrUSMP2gG9v9SJAgQPNYpzvUSxqWVBBQwdc7g52xYPe) running the full protocol on a live cluster, with real Finalized transaction signatures anyone can check on Solana Explorer. - docs/PROOF.md: init-pool -> 3 deposits -> open epoch -> 2 relayer-paid execute_action (on-chain Groth16 verify, PDA-signed NoOp) -> close, each a Finalized devnet tx with an explorer link. Plus both negatives rejected live: NullifierAlreadyUsed (Custom 16 / 0x10) and AnonymitySetTooSmall (Custom 25 / 0x19). On-chain CU measured: 108,367 for a successful action; the k_min guard rejects under-floor actions at 2,236 CU (before verification). Fee payer is always the relayer; members have no keypair, so no member wallet appears in any action tx. Honest scope stated up top: a functional devnet proof, NOT a multi-party anonymity soak, a production trusted setup, or an audit — devnet is devnet. - Wire PROOF.md into the README (devnet differentiator + docs index), docs/testing.md (Level 4), and docs/security.md (one line; limitations unchanged). No fabricated signatures: every committed step is a Finalized devnet tx verified with `solana confirm`; each negative is recorded as the RPC error + program logs (rejected at simulation, so uncommitted). Verified: cargo fmt --check + clippy -D warnings clean; cargo test --workspace --all-features green; ./demo.sh all six stages PASS.
Convert the disclosed Sybil limitation into an implemented economic mitigation, honestly bounded — priced and measured, NOT solved. Additive: entry_fee = 0 (the default) reproduces the prior permissionless-deposit behavior exactly. On-chain entry fee (the mitigation with teeth): - PoolConfig gains `entry_fee` (LE u64), appended at the struct tail so no existing field offset shifts (LEN += 8; size==LEN test still green). New accessor + `initialize(entry_fee)` param. - InitializePool carries `entry_fee` as its last field (append-only encoding). - `deposit` charges the fee into the pool PDA (fee vault) via a program-issued system transfer as a precondition; depositor + system_program accounts are consumed ONLY when entry_fee > 0, so the fee=0 account list stays `[pool]`. Underpayment / omitted fee accounts → new typed error EntryFeeUnpaid (Custom 27). Pool-data borrow is dropped before the CPI and re-taken to insert. - CLI: `init-pool --entry-fee`; `deposit` now always passes depositor + system_program (ignored when fee=0). Custom(27) documented in the error table. real-k reporting (the honest measurement): - anonymity crate: ScopeReport gains nominal_k + flagged and real_k() = worst dominance-adjusted effective-k (real-k = nominal − flagged, the largest same-funder cluster discounted — same min-entropy machinery, not a new metric). New `sybil_inflation_cost(target, honest, fee)`. - `cli sim` headlines real-k (nominal shown as a labeled secondary) and, with --entry-fee, prices the simulated inflation (e.g. 48 sybils × 1 SOL = 48 SOL). Negative/positive tests (bench `flow`, real SBF bytecode): fee omitted → 27, underpaid → 27, correct fee accepted + vault grows by exactly the fee (~21k CU). anonymity unit tests: real_k_is_nominal_minus_flagged, sybil_cost_scales_with_fee. Docs: security.md (mitigation under the existing Sybil limitation, kept intact), ARCHITECTURE.md (entry_fee + fee vault + real-k beside effective-k), anonymity.md (real-k headline + fee tie-in), README (differentiator + error row + usage), testing.md (entry-fee + real-k gates). Every doc states plainly: the fee prices Sybil inflation and real-k estimates honest anonymity; neither is a guarantee, and Sybil resistance is not "solved." Verified: cargo test --workspace --all-features green; fmt + clippy -D warnings clean (workspace and bench); ./demo.sh all six stages PASS (entry-fee enforced on real bytecode). entry_fee = 0 regresses nothing.
…ted setup
Close the biggest trust weakness — a real-but-single-operator Phase-2 ceremony —
by making it genuinely distributable and third-party verifiable, and by labeling
the independent-contributor count with total honesty. Additive; VK stays a
per-pool init parameter (not program bytecode), so the devnet program is
unchanged and docs/PROOF.md remains valid (no redeploy).
Distributable contribution flow (no shared secret; entropy never leaves a
contributor's process):
- Transcript now records, per contribution: the contributor id (bound into the
Fiat–Shamir challenge, so it cannot be re-attributed) and a prior-state hash
(explicit chaining, tamper-evident on top of the pairing same-ratio check).
- CLI: `ceremony-init` (base params + empty transcript) → `ceremony-contribute`
(fetch public params+transcript, inject fresh OS entropy, emit PoK, publish) →
`ceremony-finalize` (derive + verify the VK, print the hash to pin). Only
public data passes between operators.
- `Transcript::head()` + `independent_contributors()` (distinct ids; N self-run
steps count as ONE). One-shot `run_ceremony` labels every step the single
self-operator, so it never overstates independence.
Independent verification:
- `cli verify-setup` checks the whole chain from PUBLIC data (transcript +
verifying key): every same-ratio + Schnorr check, that the chain produces the
committed key, and prints each contributor + the independent count + the hash.
Fails loud (non-zero) on any bad step.
- `circuit/tests/trusted_setup.rs` re-pinned to the regenerated transcript and
now ASSERTS independent_contributors() == 1. New ceremony unit tests:
distinct_contributors_are_counted_independently, reattributing_a_contribution_
is_rejected, tampered_prev_state_hash_is_rejected.
Honest count — shipped key has EXACTLY 1 independent contributor:
- Regenerated the committed ceremony via the distributable flow with a single
honestly-labeled contribution ("mirror-pool-maintainer (single operator, one
machine, 2026-07)"). No independent second party was available, so none is
claimed. The key stays TESTNET-GRADE. The deliverable is the verifiable,
distributable infrastructure + the accurate count — not a bigger number.
Docs (security.md, circuit.md, README, setup/README.md, testing.md): ceremony is
now distributable + independently verifiable; exact count = 1 independent
contributor; precise assurance ("secure iff that one contributor was honest and
discarded entropy"); what remains for production (more independent contributors,
public Phase-1, published attestations). Sybil/relayer/exclusion limitations
untouched.
Re-validation: cargo test --workspace --all-features green (incl. new-key
prove+verify, re-pinned trusted_setup, verify chain); fmt + clippy -D warnings
clean (workspace + bench); ./demo.sh all stages PASS (on-chain VerifyMembership
98,634 CU; flow negatives NullifierAlreadyUsed/AnonymitySetTooSmall/EntryFeeUnpaid
all enforced). VK unchanged in the deployed program → no devnet redeploy, PROOF.md
still valid.
Close weakness #7 (anonymity never measured at scale) — honestly. Add a
reproducible, seeded `scale` CLI subcommand that sweeps the EXISTING metric
(`crates/anonymity`; no second number invented) over growing pool sizes and
Sybil pressure, and overlays the entry-fee cost. Measurement/analysis only:
no on-chain change, no confidential-value or settlement work.
`cli scale` (default seed 0x5CA1E, entry-fee 1 SOL; optional `--csv <dir>`):
- Sweep 1 — pool size (honest only), k ∈ {10,50,100,500,1000}: real-k grows
linearly with honest scale (k−1); effective-k = k.
- Sweep 2 — Sybil pressure at honest-k=100, fractions {0,25,50,75}%: real-k
(concentrated Sybils, one funder) stays pinned to the honest floor (~100)
while nominal inflates (→400) — the gap is the Sybil exposure as a curve; a
'split' column shows an adversary spreading Sybils across identities to evade
the largest-cluster heuristic (real-k inflates), which is exactly what the
per-identity entry fee prices (33/100/300 SOL to reach nominal 133/200/400).
- Prints tables + honesty header; optional CSV (no plotting dependency, so CI
stays clean).
Honesty framing at every surface (output header, docs/scale-analysis.md,
README, docs/anonymity.md, docs/security.md): the participants are SYNTHETIC —
this measures how the metric behaves as the pool grows, NOT that real
independent users will join and act in the same epoch, which stays an
operational open question. Nothing is phrased as "proven private at scale."
Determinism: verified same-seed runs are byte-identical, and the data rows are
seed-invariant under the documented model (honest = independently funded;
Sybils = one funder concentrated / one-per-Sybil split); the seed is consumed
+ recorded and that invariance is stated plainly rather than implying variance
it does not create.
Docs: new docs/scale-analysis.md (honesty block, method + distribution
assumptions, tables, reproduce command + seed); linked from README (docs index
+ anonymity differentiator) and docs/anonymity.md; new "not demonstrated with a
real crowd at scale" limitation added to docs/security.md (augmented, the
real-crowd caveat kept).
Re-validation: cargo test --workspace --all-features green (0 failures);
fmt + clippy -D warnings clean (workspace); ./demo.sh all stages PASS
(VerifyMembership 98,634 CU; flow/compliance/trace unchanged). Additive only;
the anonymity metric code and all other limitations are untouched.
Full consistency + correctness audit of the docs against the code on this branch. Docs-only + one code comment; no behavior change. Priority 1 — live devnet program id VERIFIED, unchanged: - The program keypair (target/deploy/mirror_pool_program-keypair.json) resolves to 4YrUSMP2gG9v9SJAgQPNYpzvUSxqWVBBQwdc7g52xYPe, which `solana program show` confirms deployed/live on devnet (slot 478154588). All 9 signatures in docs/PROOF.md still resolve as Finalized. No id change, no redeploy, no regeneration — PROOF.md remains truthful. No signature or id fabricated. Priority 2 — drift/overclaim fixes (fix the doc to match the code): - ARCHITECTURE.md described the trusted setup as "local dev/test only" / "production requires a ceremony" — STALE: the multi-contributor Phase-2 ceremony is implemented, distributable, and verifiable. Reconciled the circuit section, the threat-model "Trusted setup" row, the "does NOT defend" bullet, and the assurance bullet to: distributable + independently verifiable, 1 independent contributor (single operator), testnet-grade, production needs more independent contributors + public Phase-1. - ARCHITECTURE.md assurance section claimed "Minimum anonymity set is not enforced on-chain (it cannot be…)" — FALSE and self-contradictory (the threat table already says it IS enforced). Fixed to match the code: `execute_action` rejects with AnonymitySetTooSmall below k_min; bounds program-visible membership, not honest anonymity. - Added the entry_fee/real-k mitigation to ARCHITECTURE.md's Sybil threat row (was documented elsewhere but missing there). - CU number drift: testing.md said VerifyMembership "98,627"; current demo.sh measures 98,634 — updated, and cross-linked the on-chain 108,367 in PROOF.md. PROOF.md's litesvm cross-ref decoupled to "≈98.6k" to avoid re-drift. - Removed the last code comment naming the old public-seed `dev_setup` (prover.rs) — reworded to point at the ceremony, no leftover legacy language. Verified consistent (no change needed): README Custom(n) table matches error.rs exactly (0..=27, incl. EntryFeeUnpaid=27); no references to removed SPEC.md/VALIDATION.md/VALIDATION-RESULTS.md; inclusion enforcement stated as design-note-not-code (matches code); trusted-setup "1 independent contributor / testnet-grade" coherent across README/security/circuit/setup/testing/PROOF; scale analysis framed synthetic everywhere; all honest limitations (no audit, relayer trust, exclusion-as-future-work, Sybil) intact; every internal .md link resolves; References complete for all cited papers. Gates: fmt + clippy -D warnings clean (workspace, all targets). Only Rust change is a comment, so the suite green at 0eafc04 is unaffected; demo.sh unchanged (no functional/on-chain code touched).
The milestone table listed only the original M1–M8. Add a "Post-milestone passes (additive, after M8)" table so the doc reflects everything that actually exists on the branch: hardening (pool-scoped nullifiers, on-chain k_min, denominations, bench-gating), live devnet + PROOF.md, the anonymity metric, Privacy-Pools compliance, the distributable/verifiable trusted-setup ceremony (1 independent contributor, testnet-grade), the anti-Sybil entry fee + real-k, the synthetic scale analysis, and the docs consolidation + audit. Each row links its doc. M1–M8 kept intact; docs-only, no code change.
Design for direction solanabr#3: bind riverrun ID to the pool action end to end so a fund, market maker, or agent does one call (act) and gets an unlinkable, measured action, instead of stitching identity + commit + execute by hand. The proving already exists (bound_air / 1c: membership + nullifier + action in one STARK). This specifies the missing parts: a unified domain-separated derivation (id, commitment, nullifier from one root), the single act() flow that returns a receipt with the measured effective-k, the round mechanics (the honest latency-versus-crowd tension), the named limits (crowd needed, latency, committee trust today, aggregate leak), and a phased build with a falsifiable 90-day adoption check. Design only, no implementation until approved.
|
Thank you for this submission. It didn't win a prize this round, but it was one of the strongest, most technically serious entries we received, so this note is deliberately detailed. It's feedback for your future submissions, not a code review of this PR (we're not merging the entries). What you did wellYou shipped a coherent, end-to-end Rust-only ZK system: an arkworks Groth16 membership circuit, a hand-written in-circuit Poseidon gadget proven equivalent to the native/syscall hash, a real on-chain What would have made it more competitive next time1. Prove the privacy claim on real dataYour cryptography was real, but your anonymity numbers came from small seeded runs and modeled funder attribution, not real on-chain deposits and funder clusters. The biggest credibility jump: ingest actual mainnet/devnet fund flows, compute effective-k over real cohorts, and show the metric holds when the adversary uses real data. A privacy protocol is judged on whether the anonymity survives contact with reality, and that's the part that stayed synthetic here. 2. Run the ceremony as a real multi-party eventYou built the hard part, the distributable ceremony infrastructure, then ran it solo. Recruiting even two or three independent contributors and publishing the chained transcript turns "ceremony infra that could be trustless" into "a setup you can trust today." 3. Keep the live on-chain proof in lockstep with the final feature setSome guarantees were only demonstrated in local simulation, and the recorded devnet run predated later features. Every headline guarantee should have a matching live signature on the current bytecode. Before submitting, redeploy the final build and re-capture the on-chain evidence for each claim. 4. Finish the threads you startedTwo were one step short. Rust-only was proven in host tests, but the deployed artifacts were still generated by the other toolchain; landing the Rust path on-chain would have made it airtight. Compliance shipped inclusion proofs but deferred on-chain exclusion proofs; closing that completes the compliance story. When you're this close, spending the last 10% to land it is worth more than starting a new feature. 5. Consider breadth, not just depthYou hid who initiated an action very well. The strongest entries in this space also hide how much. Pairing your membership pool with a confidential-amount layer would have covered both axes instead of one. TakeawayYour technical ceiling was among the highest here. What separates this from a winning submission is proving it on real data, running the ceremony for real, keeping live evidence current with the code, and finishing the last mile. We'd be glad to see your work on future bounties. Thank you for contributing, and best of luck. |
Summary
A Rust-only implementation of
mirror-pool: a behavioral anonymity set where amember's protocol action is executed by the pool PDA, gated by a Groth16
membership proof. Observers see that an action happened but cannot attribute it
to a specific member.
Live on devnet:
4YrUSMP2gG9v9SJAgQPNYpzvUSxqWVBBQwdc7g52xYPe(explorer),
with real Finalized transaction signatures in
docs/PROOF.md.What's implemented
solana-program. No Circom / snarkjs / JS.groth16-solana(alt_bn128): VerifyMembership~98.6k CU,
execute_action~108k CU on devnet — well under budget, explicitcompute budget.
action binding (no replay, no re-target).
k_min),fixed denominations (amount bound into the proof).
reporting (min-entropy, dominance-adjusted) — measured, not claimed solved.
re-randomization + Schnorr proof-of-contribution + pairing same-ratio check,
transcript pinned by SHA-256, a
verify-setupanyone can run on public data.disclosure for compliance.
pressure (
cli scale, seeded/reproducible).Verification
cargo test --workspace --all-featuresgreen;fmt+clippy -D warningsclean;
./demo.shruns all stages end to end.(
NullifierAlreadyUsed,AnonymitySetTooSmall,EntryFeeUnpaid) indocs/PROOF.md.ARCHITECTURE.mdanddocs/.Honest scope
is distributable + verifiable; production needs more independent contributors
and a public Phase-1.
k_minbounds program-visible membership; deposits are permissionless, so theset is Sybil-inflatable. The entry fee prices this and real-k measures
it — it is not "solved."