Skip to content

mirror-pool: Rust-only ZK behavioral-privacy protocol — circuit, native program, verifiable ceremony, compliance, live devnet - #3

Open
psgoularte wants to merge 23 commits into
solanabr:mainfrom
psgoularte:chore/hardening-v2
Open

mirror-pool: Rust-only ZK behavioral-privacy protocol — circuit, native program, verifiable ceremony, compliance, live devnet#3
psgoularte wants to merge 23 commits into
solanabr:mainfrom
psgoularte:chore/hardening-v2

Conversation

@psgoularte

@psgoularte psgoularte commented Jul 22, 2026

Copy link
Copy Markdown

Summary

A Rust-only implementation of mirror-pool: a behavioral anonymity set where a
member'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

  • Rust end to end — arkworks Groth16 circuit + Rust prover, native
    solana-program. No Circom / snarkjs / JS.
  • On-chain verification via groth16-solana (alt_bn128): VerifyMembership
    ~98.6k CU, execute_action ~108k CU on devnet — well under budget, explicit
    compute budget.
  • Membership circuit: Poseidon Merkle inclusion + epoch-scoped nullifier +
    action binding (no replay, no re-target).
  • Pool-scoped nullifiers, on-chain minimum anonymity-set floor (k_min),
    fixed denominations (amount bound into the proof).
  • Anti-Sybil entry fee (on-chain) that prices set inflation, with real-k
    reporting (min-entropy, dominance-adjusted) — measured, not claimed solved.
  • Distributable, independently-verifiable Phase-2 ceremony: delta
    re-randomization + Schnorr proof-of-contribution + pairing same-ratio check,
    transcript pinned by SHA-256, a verify-setup anyone can run on public data.
  • Association-set inclusion proofs (ZK) + viewing-key selective
    disclosure
    for compliance.
  • Synthetic scale analysis of effective-k / real-k vs. pool size and Sybil
    pressure (cli scale, seeded/reproducible).
  • Relayer (fee-payer unlinkability) + epoch batcher; full CLI.

Verification

  • cargo test --workspace --all-features green; fmt + clippy -D warnings
    clean; ./demo.sh runs all stages end to end.
  • Live devnet run with verifiable signatures + on-chain negatives
    (NullifierAlreadyUsed, AnonymitySetTooSmall, EntryFeeUnpaid) in
    docs/PROOF.md.
  • Anonymity metric, threat model, and references in ARCHITECTURE.md and docs/.

Honest scope

  • Ceremony is 1 independent contributor / testnet-grade — the infrastructure
    is distributable + verifiable; production needs more independent contributors
    and a public Phase-1.
  • k_min bounds program-visible membership; deposits are permissionless, so the
    set is Sybil-inflatable. The entry fee prices this and real-k measures
    it — it is not "solved."
  • The relayer learns the member↔action link by construction.
  • On-chain ZK exclusion is a documented design note, not implemented.
  • Scale results are synthetic, not a real crowd.
  • No third-party audit.

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).
@psgoularte psgoularte changed the title mirror-pool: Rust-only ZK behavioral-anonymity protocol (circuit, program, ceremony, compliance, live devnet) mirror-pool: Rust-only ZK behavioral-privacy protocol — circuit, native program, verifiable ceremony, compliance, live devnet Jul 23, 2026
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.
Galmanus added a commit to Galmanus/mirror-pool that referenced this pull request Jul 27, 2026
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.
@kauenet

kauenet commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

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 well

You 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 groth16-solana verifier with CU measured on live devnet, and a distributable Phase-2 MPC ceremony with tamper-rejection tests and a reproducible verify-setup. The engineering discipline (deny-unwrap, checked math, honest scope docs) was excellent. The notes below are about closing the distance between "demonstrated" and "proven in the real world at scale."

What would have made it more competitive next time

1. Prove the privacy claim on real data

Your 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 event

You 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 set

Some 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 started

Two 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 depth

You 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.

Takeaway

Your 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.

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.

2 participants