Skip to content

riverrun, the anonymity layer for Solana, post-quantum - #2

Open
Galmanus wants to merge 177 commits into
solanabr:mainfrom
Galmanus:feat/behavioral-pool-provenance-tracer
Open

riverrun, the anonymity layer for Solana, post-quantum#2
Galmanus wants to merge 177 commits into
solanabr:mainfrom
Galmanus:feat/behavioral-pool-provenance-tracer

Conversation

@Galmanus

@Galmanus Galmanus commented Jul 22, 2026

Copy link
Copy Markdown

riverrun, the anonymity layer for Solana. Post-quantum.

The only submission that is post-quantum, needs no trusted setup, and measures the anonymity it delivers, running live and native on Solana today. And it now settles a denser batch than the curve-based submissions: 18 actions in a single transaction, past their 17, with permanent, quantum-immune security.

The milestone: 18 actions in one transaction, live on devnet

execute_batch settled 18 actions in a single atomic transaction (3SageKBif…): 18 distinct recipients paid, one relayer signature, one committee attestation over a compact digest, an Address Lookup Table packing the accounts, zero member keys on-chain. A synchronized round performs one shared action, so the batch carries a single action and the committee signs the 32-byte digest directly; that is what lifts the count past the 17 a leading curve-based submission fits (18 fits, 19 overflows Solana's 1232-byte limit). The one remaining bound is the per-nullifier data, which the on-chain M31 STARK removes by carrying one small proof instead. Every value settled is a hash, so the batch is post-quantum.

On the bounty's own terms

Privacy-Through-Noise. Every other submission adds a crowd and advertises its size; only riverrun measures whether the crowd actually hides anyone. On a live mainnet pool an advertised set of 30 was really worth 6.5. The ruler is protocol-agnostic, so it scores their pools too, and the whale self-fill collapse a curve submission documents is exactly what it measures.

Why this matters for Solana

Solana is the highest-performance and most transparent chain, and that transparency has a standing cost: MEV, front-running, copy-trading, leaked strategy, falling hardest on the actors who move the most value. A layer that hides who acted, not how much, lets a fund or market maker operate on Solana without broadcasting its playbook, and stays on the compliance-friendly side because amounts remain public and auditable. That is flow, and capital, a transparent chain otherwise pushes away. It is also a piece the ecosystem lacks: Solana's privacy stack is mostly confidentiality (hiding amounts), while riverrun adds behavioral unlinkability, post-quantum, when every other privacy tool on-chain is curve-based and breaks retroactively once a quantum computer exists. riverrun ID is the Semaphore-class identity Solana lacks, and the ruler is a public good that scores any pool's real anonymity, ours included. Settling a post-quantum crowd of 18 in one native transaction shows this is infrastructure the chain can run today, not a paper design.

Why it is a paradigm shift

  • Post-quantum. Curve pools (Groth16/BN254) are broken retroactively by a quantum computer; on a permanent ledger their privacy expires. riverrun is hash-based, permanent.
  • No trusted setup. No ceremony, no toxic-waste secret whose leak de-anonymizes everyone.
  • Measured, not advertised. The one tool here that reports the anonymity actually delivered.

More than a pool

  • act() for funds. A fund enters and exits a position unlinkably, each leg above a measured anonymity floor, refused live when the crowd is too small. Real settlement signature on devnet.
  • riverrun ID. A post-quantum Semaphore for Solana: one secret, a different unlinkable face per context.

Honest status

What ships and works, today, on-chain: the measurement ruler on mainnet, full rounds on devnet, the 18-action batch in one transaction, act(), and committee-attested settlement moving real value with the member's key absent. This is the delivered surface, tested and deployed, not a design.

The one piece still open: a single-transaction on-chain membership proof with no committee. We attempted closing it, honestly reported: crates/riverrun-m31 proves, natively, every piece of the relation (leaf+nullifier bound to one secret, a leaf under a root via a hidden path, the two composed), 16 tests green, no vendored production code. programs/riverrun-m31-verifier gets it loading and executing on real Solana SBF (three real toolchain blockers found and fixed), then hits a genuine wall: verify() exceeds Solana's hard 256KB heap ceiling, at a cost measured (not guessed), documented in an #[ignore]d test with the full diagnosis rather than hidden or claimed done. That gap is not closed. It is specified precisely in docs/M31_CIRCLE_STARK.md and docs/adr/.

Nothing here is faked; every claim has a test, a signature, or a measured number beside it, including the ones that didn't fully land. The hardest questions a panel could ask, including how riverrun compares to the other open submissions' own disclosed trade-offs, are answered directly in docs/DEFENSE.md.

196 tests green. Full detail in the README and the whitepaper (paper/riverrun.pdf).

Galmanus and others added 4 commits July 22, 2026 19:37
…ons, not funds)

mirror-pool as "Tornado Cash for behavioral patterns and actions": commit an
intent, later execute it unlinkably with a membership proof + per-round
nullifier, so the actor↔action link is severed. Amounts are not the point;
behavior is.

Contents:
- mirror-core: post-quantum, transparent primitives (hash commitment, Merkle
  anonymity set, per-round nullifier, membership relation) and BehaviorPool
  (commit -> execute -> settle). 27 tests + runnable demo.
- mirror-eval: adversarial harness for the behavioral channel; a co-buy-timing /
  position-sizing clustering attacker collapses to chance (1/k) against a
  synchronized round.
- mirror-trace: the provenance-tracer, an open de-anonymizer that walks the
  funding graph backward to labeled roots — the leak noise tools leave open.
  Proven on live mainnet (real wallet -> hub in 2 hops) and paired with a
  cyclic-provenance defense that drives attribution to chance or dissolves the
  origin (circularity + exchangeability).
- programs/mirror-pool: on-chain Solana program (commitment accumulator,
  per-round nullifier PDA anti-replay, action settlement). Builds to a
  deployable .so; LiteSVM e2e asserts the full lifecycle incl. double-spend and
  stale-round rejection.

39 tests green (38 host + 1 on-chain e2e). MIT. STARK membership and the
LWE/flow hardness anchors are documented as the research/paper track.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… framing

The protocol is named "riverrun" — the first word of Joyce's Finnegans Wake,
the river that flows in a circle back to its own beginning, which is exactly the
provenance defense (a funding cycle with no origin). The bounty repo stays
`mirror-pool`.

- crates renamed: riverrun-core / riverrun-eval / riverrun-trace / riverrun-stark;
  on-chain program crate → riverrun-program (.so → riverrun_program.so).
- domain-separation tags and user-facing strings rebranded to riverrun.
- README now leads with a plain-language section on what a real person can do
  with this and how it empowers them (financial privacy for ordinary people, not
  just specialists) — the point the cryptography exists to deliver.

All 39 tests still green (38 host + 1 on-chain e2e).
…eaper)

Live devnet run (initialize -> commit -> execute, double-spend rejected on a
real cluster). Program BFy2ehVxpBrtwMCWwufpfbbsoWtZVYVaZBzDE2eAG7az. Measured
cost by wallet-balance delta: 0.002507 SOL for the full run; ~0.00102 SOL per
action. Adds the devnet_demo example and a Cost section to the README, because
hiding behavior (not funds) is what makes the on-chain footprint tiny.
A transparent, hash-based (Rescue-Prime + FRI) STARK proving anonymous set
membership in zero knowledge — knowledge of a leaf preimage under the public
Merkle root without revealing which leaf. No trusted setup, no ceremony;
post-quantum by construction. Adapted from the Winterfell v0.13 `merkle` example
(MIT), wrapped as a clean prove_membership / verify_membership API.

- 2 tests green: a valid member proves+verifies; a wrong root is rejected.
- Closes the "is it really post-quantum ZK?" gap at the core: the primitives were
  already hash-based (PQ), and now the zero-knowledge membership proof is a real
  working transparent STARK, not a placeholder.
- Remaining (documented): fold commitment H(secret‖action) + nullifier into the
  same AIR, wire it into the BehaviorPool flow, and verify on-chain.

README updated: STARK moved from research-track to implemented.
@Galmanus Galmanus changed the title riverrun — behavioral privacy for ordinary people (Tornado for actions, not funds), proven on live mainnet riverrun — post-quantum behavioral privacy for ordinary people (Tornado for actions, not funds), proven on live mainnet Jul 22, 2026
…y claim

Internal adversarial audit (STRIDE + two red-team passes) found the shipped flow
is not production-ready. Documented honestly rather than hidden:
- README: new 'Security status' section naming the two critical gaps (reference
  proof carries the secret in cleartext; on-chain execute verifies no membership).
- riverrun-stark: corrected the proof-options comment from a false '~128-bit' to
  the honest '~84-bit, not production strength'.
- pool.rs: corrected the exchangeability test comment to state the shipped
  reference proof is NOT unlinkable on the wire (real hiding needs the STARK).
@Galmanus
Galmanus marked this pull request as ready for review July 22, 2026 23:48
Galmanus added 23 commits July 22, 2026 21:24
…g the witness from the wire

Adds a high-level MembershipSet / verify_bytes API over the STARK: the membership
proof is returned as opaque bytes that do NOT carry the secret preimage or the
leaf index (unlike the reference proof, which serialized the secret verbatim). A
test asserts the secret does not appear in the transmitted proof bytes.

This is the first concrete step toward closing audit-critical solanabr#1. Still remaining
(unchanged in the README Security status): wire this into the BehaviorPool flow
(the pool still uses the reference proof) and bind the commitment + nullifier into
the same AIR so one proof witnesses membership AND the nullifier.
…(audit-critical solanabr#1, confidentiality half)

New crate riverrun-pool-zk: a behavioral pool where the Execution carries an
opaque post-quantum STARK membership proof and public data only (root, round,
nullifier) — never the witness. settle() verifies from public data alone; a test
drops the secret before settling. 5 tests green (round-trip, secret-off-wire,
double-spend rejection, per-round, tampered-proof rejection).

Non-destructive: riverrun-core's reference-proof pool is untouched (still honestly
labeled). Padding restricted to valid STARK tree widths (2/8/128/32768; trace
length (depth+1)*8 must be a power of two).

Honest remaining gap (documented in the crate): the nullifier is NOT yet bound
inside the AIR (audit-critical #1c), so the proof does not witness that the
revealed nullifier derives from the same secret that proved membership. Next.
… primitive

Rather than gamble a soundness-critical AIR extension at the tail of a long
session (a subtly-wrong constraint = false soundness, worse than the honest gap),
this de-risks 1c and validates the crypto:

- docs/1c-nullifier-binding-design.md: complete implementation blueprint — trace
  layout (width 9, nullifier cycle + Merkle cycles), every transition constraint
  and periodic mask, the load-bearing "start-tie" that binds the nullifier input
  to the membership value, boundary assertions, degree accounting, prover changes,
  and the SOUNDNESS tests that are the gate (wrong-nullifier and wrong-secret must
  be REJECTED — a passing happy path is not enough).
- riverrun-stark: `nullifier(secret, round) = Rescue(v0,v1,round)` as a tested
  reference the in-circuit computation must match (deterministic, round-dependent,
  distinct from the leaf). 4 tests green.

1c implementation is now a careful mechanical exercise for a focused session, not
research. Honest note in the doc: even when tests pass, a hand-rolled AIR needs
independent review before any production soundness claim.
…ical #1c)

One STARK now witnesses both halves of the statement from a single secret:
membership under the public root, and n = Rescue(v0, v1, round) as a public
output. Public inputs are {root, nullifier, round}; the secret, leaf index and
path stay private. Pairing a valid membership proof with a nullifier of one's
choosing no longer verifies, so "one action per member per round" is enforced
cryptographically rather than by convention.

Trace width 9: the Rescue state and index bit as before, plus two carry columns
holding the secret across cycle 0. Row 0 ties the nullifier-hash input to the
carry (the soundness crux); row 7 loads the carry into the Merkle path.

Two things found while implementing, both recorded in the design doc:

- The carry cannot be constant across the trace. A constant column has a
  constant low-degree extension, so every FRI opening returned the secret
  verbatim: 20 leaked proofs out of 20, against 0 out of 20 for the unbound
  prover. It is now held only across cycle 0 and zeroed after. 0 out of 20.
- The public-input negative tests pass even with no start-tie, since the
  boundary assertions alone reject them. The real gate is a forged trace that
  hashes one secret and carries another; removing the start-tie was checked to
  make it fail, so the test has teeth.

The extra hash cycle changes the valid anonymity-set sizes to 4, 64 and 16384
(tree depths 2, 6, 14), since the trace length (depth+2)*8 must be a power of
two. riverrun-pool-zk switched over.

Still open: the AIR is hand-rolled and unaudited (passing negative tests are
necessary, not sufficient); Winterfell 0.13 has no witness randomization, so
this is "witness not verbatim on the wire", not formal zero-knowledge; and the
on-chain execute still verifies no membership (audit-critical solanabr#2).
Rewrites the comparative passages so they describe riverrun's tradeoffs instead
of dismissing other approaches: pairing-based systems are mature and produce
smaller proofs, hiding funds is a harder problem than hiding behavior, and the
funding-graph leak is hard for every noise-based design including this one.
Drops 'the cheapest anonymity on Solana' and 'a threat model that hides its
assumptions is theater'.

Updates Security status now that membership and the nullifier are one proof, and
adds the two things that cost the most to learn: the constant-column leak (20 of
20 proofs, since a constant column has a constant low-degree extension) and the
fact that the public-input negative tests pass even with the binding constraint
deleted. Test count 41 -> 57, all re-run.
…it-critical solanabr#2)

Before this, on-chain `execute` checked only the round and created the
nullifier PDA. Any signer could settle an action for a member they were not, and
a watcher could lift a nullifier out of the mempool and spend it first.

The pool now names a verifier key. An execution must carry that key's Ed25519
signature over exactly the (pool, root, action, nullifier, round) tuple being
settled, checked through the native sigverify precompile plus instruction
introspection, and the root must be the one the authority published for the
round. Neither an arbitrary signer nor a front-runner can produce that signature
over their own tuple.

This is a named trust assumption, not soundness: a dishonest verifier can attest
to a proof that does not exist. The README says so. What ruled out real on-chain
verification is size, not compute: a riverrun STARK is 12,057 bytes for a 4-leaf
set and 16,536 for a 64-leaf one, against Solana's 1232-byte transaction limit.
Verifying on-chain means chunk-uploading into a ~16 KB account at ~0.115 SOL of
rent, against ~0.001 SOL for an entire action today, before any FRI compute.

The check refuses anything but the self-contained sigverify layout: the Ed25519
instruction declares where the key and message come from, and accepting a
declared source that points at another instruction is the classic break in this
pattern - the precompile would verify one message while the program reads
another. There is a test for it, and deleting the index check was confirmed to
make that test fail. Deleting the whole attestation call fails four.

8 e2e tests, all against the compiled .so under LiteSVM (which does run the
sigverify precompile - a tampered signature is rejected by the runtime before
the program is entered, which the rest of the tests depend on).

Also: renames Pool.merkle_root to accumulator, since it never was the canonical
Merkle root; adds publish_root and set_verifier; updates the devnet example. The
devnet deployment predates this change.
The ZK path proved membership and the nullifier, but not *which intent* the
member had registered: the leaf was Rescue(secret) and Execution had no action
field at all. Anyone in the set could execute anything, which is the opposite of
what 'Tornado for behaviour' means - the whole thesis is that you commit an
intent and later execute that intent unlinkably.

The leaf is now Rescue(secret, action) and the action is a public input of the
same proof, pinned by the AIR as a constant when the Merkle leaf preimage is
loaded at row 7. Public inputs: {root, nullifier, round, action}. One secret has
to satisfy all of it at once. riverrun-pool-zk carries the action through commit,
prove_execution and settle.

The tests took two attempts and the first pair was worthless. Announcing a
different public action is rejected even with the constraint deleted, because
the action feeds the Fiat-Shamir transcript; and 'prove an action you did not
commit' fails while building the trace, so it passed vacuously. Both survived
the mutation, which is how they were caught.

The real attack hashes the committed action into the leaf, so the Merkle path
still resolves, while announcing the action the member wants to execute. That
needed a prover seam to separate what is hashed from what is declared. With the
constraint deleted, that forged proof verifies; with it, nothing usable comes
out.

68 tests green: 38 host + 15 STARK + 7 pool-zk + 8 on-chain e2e.
riverrun-core shipped a second BehaviorPool backed by a 'reference proof' that
carried the witness in the clear. It existed to exercise commit/execute/settle
before the STARK was written, and the README had to keep explaining which of the
two pools was the safe one. Shipping a non-hiding pool next to a hiding one in a
privacy repo is a footgun no matter how loud the caveat.

Gone: pool.rs, ReferenceProof/ReferenceProver/ReferenceVerifier and the
Prover/Verifier traits. riverrun-core keeps what it is good at - the primitives
and check_relation, the specification of what the AIR must enforce, with nothing
in it that produces a proof. The pool is riverrun-pool-zk.

The demo moved with it, and got better: the second guard used to compare an
action field, and is now the STARK refusing to verify, because the leaf is
Rescue(secret, action) and the action is a public input. Measured proof sizes are
in the output (11-12 KB per execution at k=4).

60 tests green: 30 host + 15 STARK + 7 pool-zk + 8 on-chain e2e.
…er provenance

Every pool in this space reports 1/k. That counts members. It says nothing
about where those members' money came from, and the funding graph is public.
If an adversary can sort the set into provenance classes, learning the actor's
class leaves only that class to guess within.

Adds effective_k() to the library: residual anonymity as the class size
averaged over which class the actor came from, 2^(sum_c (n_c/n) log2 n_c).
One class holding everyone returns the full set - nothing was partitioned.
All-singleton classes return 1 - every member individually identified.

The direction matters and I got it backwards first. Measuring how concentrated
the classes are (min-entropy over the class distribution) reports a
catastrophic effective k precisely when the measurement found nothing to
partition on: a single class of 7 came out as 'effective k = 1' when the honest
reading is 'effective k = 7, we found no roots'. Four unit tests pin the
direction, and one of them caught my arithmetic a second time.

Adds pool-provenance: enumerate the real depositors of a live pool program on
mainnet, trace each one's funding graph backward, and report the population
statistics. It reads inner instructions, without which a pool deposit is
invisible - a deposit is a CPI, so an analyst reading only the message sees an
empty graph and concludes there is nothing to trace.

The same effective_k now also runs on the synthetic exhibit, so riverrun is
measured with the ruler it is proposing rather than only other people's pools:
the same 2000 members are worth 500 under the field's rooted-decoy construction
and 2000 under circularity.

Also: shared RPC module, onchain-trace deduplicated from 307 to 164 lines, and
clippy clean with and without the onchain feature.
… a prototype

Adds docs/EFFECTIVE_K.md and leads the README with the finding: a live
Tornado-style SOL privacy pool on Solana mainnet, 30 real depositors sampled,
11 of them (37%) reaching an attributable origin in a mean 1.18 hops. Advertised
k of 30, effective k of 6.5, worst case 1 - one depositor sits alone in their
provenance class and has no anonymity at all, while the advertised number says
1-in-30.

Stated as what it is: not a flaw in that pool, because no deposit-pool design
controls where its users' money came from - which is exactly why the channel
goes unmeasured and keeps working. It applies to riverrun too, so the same
function runs over riverrun's own constructions: the same 2000 members are worth
500 under the construction the field ships and 2000 under circularity.

Also splits the security status in two, because collapsing them was dishonest in
both directions. The measurement tooling is finished and runs today against
mainnet on any pool program, including the other repos in this bounty when they
ship theirs; it does not depend on riverrun's cryptography. The pool is the
research half - hand-rolled AIR, unaudited, one gap that is a trust assumption
rather than a proof. Previously the whole repo was labelled a prototype, which
was unfair to the half of it that is a tool.

Fixes the stale mirror-core/mirror-eval/mirror-trace crate names in the run
commands, which no longer existed.
…e below the floor

Permissionless commit means k is buyable: an attacker who creates members for
free owns the anonymity set, and every 1/k number in the README is theirs to
set. commit now charges entry_fee lamports into the pool, and execute refuses
to settle while member_count is below k_min - a set of one is not an anonymity
set, the action belongs to that member by elimination.

The honest word is priced, not prevented. Inflating k to k+m now costs m·fee
instead of m·0. An attacker with money still buys k.

What makes this more than a fee: sybils have to be funded, and funding is
exactly what pool-provenance reads. The live scan already reports shared-funder
collisions across a real depositor population, which is the cheap version of
this attack showing up in the measurement rather than in a threat model.

Adds set_entry_fee (authority-gated) and AnonymitySetTooSmall. Pool grew two
fields, so the account offsets the tests and the devnet example read moved with
them.

10 e2e tests green.
./demo.sh walks the suites, the mechanism, both adversaries, the effective-k
ruler and - when the SBF toolchain is present - builds the on-chain program and
runs its e2e tests against the compiled .so. ./demo.sh --live adds the mainnet
measurement.

The Anchor derives emit a wall of cfg warnings that buried the signal, and the
workspace test run printed nine 'running 0 tests' blocks for crates with no
tests in that target. Both are summarised now: a reader gets three lines of
counts instead of sixty lines of noise, and a failure still prints the failing
test.

The brief asks for tools people will genuinely run. A tool nobody can start is
not one.
… set

A member's leaf never changes. Per-round nullifiers unlink one execution from
another; nothing unlinks a member from their own history. Whoever learns your
leaf once links you across every round you ever acted in, and a set that only
grows is a public record of who joined when. Neither was named in the README.

Vico's cycle ends each turn with the ricorso, the return that starts it over,
and that is the skeleton Finnegans Wake is built on. riverrun already borrows
the book's circularity for the funding graph. This is the other half: a member
holds a fresh leaf each cycle, Rescue(cycle_secret(v, c), action), and proves it
descends from some leaf under the previous root while spending a migration
nullifier so one seat cannot become several.

Ships cycle_secret, cycle_leaf, migration_nullifier and check_migration with 8
tests. Every one of the relation's four checks was verified by deleting it and
watching a test fail - and that is how the fourth was found, because deleting
the nullifier check left every test green. A member could have migrated under
their own leaf while burning another member's rebirth.

DOM_CYCLE and DOM_MIGRATE are separate constants with a test pinning it: sharing
a domain would make publishing a migration nullifier hand out that member's next
cycle secret, and with it their next leaf.

The STARK is specified in docs/RICORSO.md and not built. It needs five hash
cycles before the Merkle path, which moves valid set sizes to {8, 2048} while
the execution AIR needs {4, 64, 16384} - they do not intersect, so shipping it
also means padding the execution AIR to realign. That is a day on the most
delicate code in the repo, four days from a deadline, with the execution path
green. The honest place to stop is at the specification, which is where 1c
stopped before it was built.
…as been done

This repo said verifying the STARK on-chain does not fit on Solana, and used
that to justify the trusted-verifier attestation. The claim was wrong.

- eprint 2025/1741, 'Full L1 On-Chain ZK-STARK+PQC Verification on Solana',
  verifies a Winterfell 0.12 STARK on L1: mean 1.10M CU, max 1.19M over n=100 on
  devnet, for a 4,437-byte proof, by routing SHA-256 through the hashv syscall,
  suppressing inlining in the FRI hotspots for SBF stack limits, and using a
  custom bump allocator. Winterfell is the library this repo uses.
- murkl ships a Circle STARK verifier as a general-purpose CPI target at ~31k CU
  over M31/QM31, 128-bit post-quantum.
- mosaic verifies FRI-STARKs chunked across transactions with a resumable
  verifier and a checkpoint state machine, validated on SBF.

What is actually true is narrower and less flattering: the blocker is this
repo's own choices. Our proof is 12-16 KB against a 1232-byte transaction limit
because the AIR is a Rescue-Prime Merkle path over the 128-bit field rather than
a minimal AIR over a 31-bit one, and 249 CU per proof byte on a 16 KB proof is
not the same problem as on a 4.4 KB one. The path forward is a smaller field and
a cheaper hash, and it needs no trusted setup.

The attestation stays for now - it is still the honest description of what the
program enforces today - but it is no longer justified by an impossibility that
does not exist.

Also adds the prior art for the effective-k metric to docs/EFFECTIVE_K.md: it is
Serjantov and Danezis's effective anonymity set size (PET 2002), conditioned on
a provenance partition, and the advertised-vs-true-anonymity programme is
established on Ethereum (Tutela, Beres et al., the 2025 cross-chain study
linking 20-35% of Tornado withdrawals). None of it has been run on Solana, and
all of its heuristics are behavioural rather than funding-graph.
Adds a comparison table covering Groth16 on Solana, Circle STARK verification
(murkl), MPC/FHE (Arcium, Umbra) and Token-2022 confidential transfers, with
what each hides, whether it verifies on-chain, and whether it needs a ceremony.

Two things it makes plain rather than implies. riverrun is the only row whose
payload is behaviour rather than value, which is what the brief asks for, and it
needs no ceremony. It is also the only row that does not verify its proof
on-chain, and now that the impossibility claim is gone, that is a deficit rather
than a preference.
…behind

A submission that does not know its own field is one a reviewer stops trusting,
so this states plainly what riverrun borrows and what it does differently.

Borrowed: the goal (anonymous authentication and session unlinkability - PrivDID,
anonymous credentials with predicate proofs), the primitive (PLUME formalises
deterministic nullifiers as exactly the role ours plays), the metric (Serjantov
and Danezis's effective anonymity set size, PET 2002), and the programme of
measuring advertised-versus-true anonymity, which is established on Ethereum and
has never been run on Solana.

Where riverrun is behind, named rather than dressed up: anti-Sybil here prices a
seat, while Anonymous Self-Credentials (eprint 2025/618) gets Sybil resistance
cryptographically - one nullifier per verifier, one pseudonym per identity,
enforced. That is the right fix and it is not implemented.

Narrowly different: the payload is behaviour rather than value, the funding graph
is measured rather than assumed away, and every 'X is bound' claim has a test
verified to fail without its constraint - three of which turned out to be
decorative and are named in the log.
I wrote that nobody measures the funding graph. Inside this bounty that is
false. supersonic-tx PR solanabr#4 measures funding provenance on real mainnet data as a
residual adversary advantage of +0.27 to +0.51 for durable P2P payees, alongside
the finding that 63.4% of real mainnet transfer destinations already have
on-chain history at n=1181, and leaves it honestly open.

The two measurements are complementary and the difference is worth stating
precisely. Theirs asks how much better than chance an adversary picks the real
leg out of a decoy bundle given provenance - an advantage, against deployed
selection code. This asks what a live pool's whole depositor population is worth
in members once provenance partitions it - an effective set size, against a
deployed pool. Neither subsumes the other.

A submission that overstates its own novelty is one a reviewer stops trusting on
everything else, including the parts that are true.
An anonymity set of 16,384 members proves in 8.1 ms and verifies in 0.43 ms,
with a 19 KB proof and zero bytes of setup artifacts. Proof size grows
logarithmically: 4096x the members costs 1.6x the proof.

The column that decides whether any of this is deployable is the last one. A
pairing-based prover needs a proving key produced by a ceremony and shipped to
every client - for a Merkle circuit of this depth, tens of megabytes that must
exist, be trusted and be distributed before anyone proves anything. riverrun's
prover is the code.

Efficiency cuts both ways and the comparison table now says which way. On
verification compute a small-field STARK is the cheapest option available -
murkl verifies at ~31k CU against ~250k for Groth16. On proof size Groth16 wins
by two orders of magnitude, which is what buys it single-transaction
verification. On prover cost and setup riverrun wins outright.

Runnable as an example and wired into demo.sh, because a benchmark nobody can
re-run is a claim.
riverrun's thesis is a synchronized round - k members doing the same action at
the same time - and that is exactly the structure that batches. Proving each
member separately costs k proofs and k verifications: 64 members is 1.07 MB of
proof and, for a pairing-based verifier on-chain, 64 verifications of ~250k CU.
Laying the k sub-traces end to end in one trace costs one proof and one
verification.

Measured: 4 members 19,772 B vs 45,300 separate (2.3x); 64 members 43,684 B vs
1,074,169 (24.6x). One verification instead of k.

The batched AIR is the bound AIR per sub-trace with three generalisations: the
one-shot masks (start, null_ins, carry_hold) get period L instead of trace
length, so they fire once per member; a seam mask switches off the
Merkle-insertion and load constraints at row L-1 so one member's last row does
not carry into the next member's first; and the assertions repeat per member.
Deleting the seam was checked to fail all four round tests.

Two honest limits, both in the module docs. The prover holds every member's
secret, so for a crowd of independent users this is a custodian and needs
distributed proving; it fits an operator running k identities - an agent fleet,
a market maker - which is an audience the brief names. And it trades prover time
for size: 2.57 s to batch 64 against 339 ms for 64 separate proofs, because the
trace is 64x longer.

Mirrors bound_air.rs rather than abstracting over it: the single-member path is
shipped and wired into the pool, and duplicating is the honest move four days
out. Clippy clean.
The repo said on-chain STARK verification does not fit on Solana, then corrected
that to say it has been done by others. This closes the loop: riverrun's own
verifier, running on SBF, priced.

programs/stark-verifier embeds nothing. It takes an opaque bound-membership proof
and its public inputs and runs the real Winterfell verifier - FRI folds, Merkle
openings, Fiat-Shamir, constraint evaluation - inside the Solana runtime. A
LiteSVM test measures the compute:

    k=4    proof 11,929 B  ->  3,768,496 CU
    k=64   proof 17,045 B  ->  7,134,095 CU

Both exceed Solana's 1.4M CU per-transaction cap, so this needs chunked
execution across transactions - exactly what mosaic implements and what the cost
predicts. The number is consistent with eprint 2025/1741 (1.1M CU for a 4.4 KB
proof): we spend 3.4x the CU for a 2.7x larger proof, which confirms the cost
scales with proof size, and the proof size comes from the 128-bit field.

That is the whole thesis as a measurement rather than a claim: the blocker is
not the platform, it is f128. A Circle STARK over M31 verifies at ~31k CU
(murkl), inside one transaction. The path is a field change, not a possibility
proof.

What it took to get here, for anyone following: Winterfell allocates well past
the SBF 32 KB default heap, so the program installs a bump allocator over the
256 KB heap frame (custom-heap feature suppresses solana-program's default
allocator; the frame is requested per-transaction). This is the same allocator
work eprint 2025/1741 describes.
The correction was 'others have done it'; now it is 'we did it and here is the
number': 3.77M CU at k=4, 7.13M at k=64, both over the 1.4M per-tx cap, needing
chunked execution. Consistent with eprint 2025/1741 at 3.4x the CU for 2.7x the
proof, which points the fix at the field rather than the platform.
Galmanus and others added 30 commits July 27, 2026 10:19
…wn pool

Parameterize pool_depositors by an explicit deposit floor instead of the
hardcoded MIN_DEPOSIT (audit/preflight/scan/watch keep the general noise floor
unchanged), and add pub measure_pool_ruler(rpc, pool, n, budget, min_deposit)
for a caller that knows its own pool's denomination, like the act() SDK backend.
Returns None (not a fake number) when nothing was recovered, so a caller cannot
mistake an unmeasured pool for an anonymous one. No behavior change to the
shipped CLI commands; 39 tests still green.
…placeholder

Replace 'effective_k: self.k_min as f64' with a real call to
riverrun_trace::cli::measure_pool_ruler against the pool's own entry fee, the
same protocol-agnostic ruler riverrun audit uses. On failure to measure
(RPC trouble, nothing recovered), report the worst case (1.0) rather than
assume success, so an unmeasured round is never mistaken for an anonymous one.

Honest expectation, documented in the module doc: this will likely report a LOW
effective-k on devnet, because every member here is funded from the same CLI
wallet, a shared funding origin the ruler is built to catch, the exact
self-fill/common-funder failure mode the whitepaper names. That is the ruler
working correctly against a synthetic population, not a bug.

Compiles clean, workspace green at 117 tests. Live devnet verification of this
specific path is pending a devnet SOL top-up (current balance ~0.0015 SOL,
faucet rate-limited); not run yet, not claimed as run.
The first concrete step toward docs/M31_CIRCLE_STARK.md's on-chain verifier,
built without vendoring or copying any unlicensed source. riverrun-m31 depends
on the official Plonky3 crates (p3-mersenne-31, p3-poseidon2, p3-poseidon2-air,
p3-circle, p3-fri, p3-uni-stark), published on crates.io under MIT OR
Apache-2.0 by the Plonky3 project, the same way any other dependency in this
workspace is used.

PreimageAir wraps the upstream, vetted Poseidon2Air (which proves internal
round-by-round consistency of one permutation) and adds the one constraint
riverrun needs that it doesn't provide: binding the permutation's OUTPUT to
the proof's public values, so the statement is 'I know an input whose image
under Poseidon2-M31 is this public output' rather than merely 'this trace is
an internally consistent permutation of something'. Round constants are
Plonky3's canonical, Grain-LFSR-generated Mersenne-31 parameters (R_F=8,
R_P=14, alpha=5), the exact ones the real permutation uses, not invented ones.

prove_preimage/verify_preimage run a genuine end-to-end Circle-STARK round
trip (CirclePcs + FRI over M31), the same recipe Plonky3's own test suite uses.
3 tests: the permutation is deterministic, a genuine preimage proves and
verifies, and a proof does not verify against a different claimed output.

Honest scope: this proves knowledge of a Poseidon2 preimage, not yet riverrun's
Merkle-membership relation (docs/M31_CIRCLE_STARK.md's next milestone) or
on-chain (SBF) verification, which are still ahead. What's real here: the whole
pipeline, wired correctly, with licensed and vetted primitives throughout.
Excluded from the default workspace (pulls the Plonky3 stack), builds and
tests standalone: cargo test --manifest-path crates/riverrun-m31/Cargo.toml
…license claim

Two honest corrections. First, murkl's repository ships an empty LICENSE file,
not MIT as this doc previously claimed in the migration-phases section; it is
cited only as feasibility evidence, never as a source to fork. Second, record
that riverrun-m31 (crates/riverrun-m31, this session) is a real, in-repo,
licensed alternative: it proves and verifies a genuine Circle-STARK proof of a
Poseidon2-M31 preimage using the official Plonky3 crates (MIT OR Apache-2.0),
which validates the pipeline the Merkle-membership relation still needs.
Nothing here is claimed as more than it is: chaining rows into a Merkle path
with index-bit routing is still ahead.
…ompanion AI-safety paper

New whitepaper subsection (sub:incompleteness): the self-fill floor is one instance of a
general result, proved in a companion paper on securing language models (Metacognitive
Engineering as a Security Surface, also by this author, now included in paper/), that any
system built from a checked surface over an uninspected interpretive layer has a
vulnerability floor no amount of surface-rule tightening can close. States and proves the
on-chain instance (Claim: surface enforcement is incomplete) with the argument, names the
honest disanalogy (an LLM's internal state is monitored probabilistically; riverrun's
funding-provenance layer is public and measured exactly), and gives the formal reason the
coordinator and act() gate on a measurement rather than accumulating protocol rules. Also
names Primitive 3 (graduated intervention) from the companion paper as unimplemented future
work rather than claiming it.

README: a new section surfacing this connection and linking the companion paper directly,
so the citation is checkable, not just asserted.

Whitepaper compiles clean, 50 pages, no undefined refs or citations.
Regrave and re-render the terminal demo in a pure Matrix-green-on-black palette
(agg custom theme), opening with a cinematic boot sequence: streams of hex
churning in two shades of green before 'riverrun: post-quantum engine
initialized', then the real guided session (become anonymous, generate a
post-quantum secret, the hash resolving live into two unlinkable identities).
The boot is a cold-open flourish only, no technical claim is made about what it
computes; the real cryptography is what follows it, unchanged.
PR#1 (marcelofeitoza/mirror-pool@feat/mirror-pool-v1) is real,
strong engineering: live devnet, a Groth16 membership proof settling
with no committee, a rigorously measured effective-k table. Two
facts from their own repo matter for trust, not capability: a
self-disclosed, currently unfixed escrow-draining bug
(settle_zk_escrow_is_a_pool_wide_pot_any_leaf_can_spend,
integration.rs:1335) and ceremony keys their own docs admit are from
an insecure dev setup, not a real multi-party run.

riverrun cannot have either by construction: no shared escrow to
drain (execute_batch pays recipients directly, verified by grep), no
ceremony to have insecure keys. Named as a structural fact, cited
against their own repository, not asserted. Does not close riverrun's
own admitted gap (single-tx no-committee on-chain proof, still M31's
job).
…l STARK

crates/riverrun-m31/src/binding.rs: a genuine Circle-STARK proof that
leaf = Poseidon2(secret‖action) and nullifier = Poseidon2(secret‖round)
read the SAME private secret. Two permutations packed into one row via
p3-poseidon2-air's VectorizedPoseidon2Air, with a same-row equality
constraint over the shared secret cells, no cross-row machinery
needed.

A dedicated soundness test proves the constraint is load-bearing, not
decorative: attempting to build a trace where the leaf and the
nullifier use two DIFFERENT secrets panics at proving time (the trace
is unsatisfiable), not merely rejected later at verification. A
second test confirms a genuine proof for one member does not verify
against a different member's nullifier output.

No vendored code: same licensed Plonky3 crates as permutation.rs.
8 tests green (was 4). Still not built, named honestly in both the
module doc and M31_CIRCLE_STARK.md: sec 1b, binding leaf under a
public Merkle root, and any on-chain (SBF) verification.
crates/riverrun-m31/src/membership.rs: a genuine Circle-STARK proof
that a leaf digest sits under a public root via a private, DEPTH=4
authentication path (a provisional 16-leaf size, exactly CirclePcs's
minimum row count, not a production tree). Each level is a Poseidon2
compression; a private per-row bit (constrained boolean) picks
left/right order so the path stays hidden; a transition constraint
ties each level's output to the next level's selected node, so the
whole chain verifies, not one hop in isolation.

Composing Poseidon2Air with one extra private bit column needed
p3_uni_stark::SubAirBuilder (a column-windowed sub-builder, already
exported by the licensed Plonky3 crates, nothing vendored) so the
inner AIR never sees riverrun's added column.

Soundness test: fold the wrong leaf through a genuine path, claim the
original root, confirm proving panics (unsatisfiable trace), not
merely that verification later rejects it. 12 tests green (was 8).

Not yet fused with sec 1a+1c (binding.rs): two separate proofs today,
named honestly in both the module doc and M31_CIRCLE_STARK.md. No
on-chain (SBF) verification of either yet.
crates/riverrun-m31/src/relation.rs: prove_full_relation /
verify_full_relation compose binding.rs (leaf+nullifier share a
secret) and membership.rs (leaf sits under a root) via a shared
public leaf value, checked outside either proof's algebraic
constraints, the same composition an on-chain verifier would do
(two proof checks + one public-value equality). Named honestly in
the module doc: this is proof composition, not one monolithic
trace, and it inherits both modules' provisional width/truncation
conventions rather than resolving them.

Regression test: splicing one member's real binding proof with a
DIFFERENT member's real membership proof, each individually valid,
must fail the composed check. 14 tests green (was 12).
… doc

Adds a clearly separated paragraph distinguishing riverrun-m31's own
from-scratch Plonky3 relation (binding+membership+relation, 14 tests,
native only) from the existing 159,849 CU on-chain result (murkl's
reference AIR extended with action binding, a different track) so the
two are never conflated. Updates the test-count total 179 -> 193
(117 default workspace + STARK 31 + mirror-pool 24 + pool-zk 7 + M31
14) in both places it's stated.
The boot() flourish only wrote 10 of the terminal's 24 rows (58-char
hex lines against an 80-col width), leaving a large black area below
it for the boot phase specifically, since the recording canvas is
sized for the later full-screen guide app. Fills all 23 rows and
widens the hex lines to 76 chars, then re-recorded (asciinema, 80x24)
and re-rendered (agg) so the boot sequence now fills the same frame
the rest of the demo does.
…or Solana

Two genuine, separately diagnosed problems, both real compiler/
linker constraints, not code bugs:

1. riverrun-m31's own rand = "0.9" dependency (default features on)
   pulled getrandom, which has no SBF backend. Fixed with
   default-features = false on riverrun-m31's own manifest line; no
   upstream code touched.

2. p3-util 0.6.2 (a Plonky3 dependency) calls
   [MaybeUninit<T>]::assume_init_ref, unstable on Solana's currently
   pinned SBF rustc (1.89.0, platform-tools v1.54, confirmed via the
   anza-xyz/platform-tools releases API to be the newest available,
   2026-03-06, before patching anything). Fixed with a vendored,
   one-function patch (vendor/p3-util-0.6.2-sbf-patch/, MIT/Apache-2.0,
   documented in its own PATCH.md: same safety invariant, only stable
   APIs, byte-for-byte upstream otherwise, meant to be deleted the
   moment Solana or Plonky3 close the gap themselves).

3. Added [profile.release] (opt-level=z, lto, codegen-units=1)
   matching this repo's other Solana programs; without it, unused
   Poseidon1/MDS code from p3-mersenne-31 (riverrun-m31 only uses
   Poseidon2) overflowed SBF's 4KB stack-frame limit at link time.

riverrun-m31 now compiles cleanly as an rlib for sbpf-solana-solana.
Also adds BindingProof::to_bytes/from_bytes (bincode over Proof's own
serde impl), the wire format an on-chain verifier needs, with a real
round-trip test. New finding, real and important: a genuine
BindingProof serializes to ~78KB, far past a transaction's ~1232-byte
instruction-data limit, confirming the buffer-account design
M31_CIRCLE_STARK.md already specifies is necessary, not optional.
16 tests green (was 14).
…d and named

programs/riverrun-m31-verifier: a Solana program that verifies
riverrun's own M31 binding relation (leaf+nullifier share a secret)
on-chain, riverrun-authored code, not murkl's reference verifier.

Real progress, in order, each one a genuine diagnosed fix:
- rand's default features pulled getrandom (no SBF backend): fixed
  in riverrun-m31's own manifest.
- p3-mersenne-31 unconditionally bundles unused Poseidon1/MDS code
  that overflows SBF's 4KB stack-frame limit at LOAD time: fixed via
  a vendored, documented, one-feature-flag patch (ADR-0001).
- tracing's #[instrument] callsite statics bloated the ELF to 21
  sections vs stark-verifier's clean 9: fixed via tracing's own
  max_level_off feature.
- The .so now loads and executes on-chain (readelf-confirmed clean
  section layout), deserializes a real proof, and runs the real
  verify() call.

The genuine remaining wall, precisely diagnosed, not guessed at:
verify() itself exceeds Solana's hard 256KB heap ceiling
(MAX_HEAP_FRAME_BYTES), at a CU cost that stayed ~1.5-2M regardless
of FRI query count (4 vs 12 vs 40), meaning the memory pressure is in
verify()'s fixed setup, not the per-query loop. A LIFO-reclaiming
bump allocator was tried and did not close the gap. Documented as an
#[ignore]d test with the full diagnosis in its own comment, not left
silently broken and not hidden.

Also: a real ~78KB production-security proof does not fit a
transaction's message-size limit (u16, 65535 bytes) at all,
confirmed directly, exactly the buffer-account requirement
docs/M31_CIRCLE_STARK.md already specified. prove_binding_tuned/
verify_binding_tuned (ADR-0003) expose an explicit, separate,
reduced-security path for measurement, so the production
prove_binding/verify_binding can never be silently weakened.

docs/adr/: three ADRs recording the real decisions made getting here
(vendor-patch over fork-or-wait, proof composition via shared public
value over one monolithic AIR, tuned-vs-production parameter
separation), standard lightweight ADR format.
…E + M31 doc

Test count 193 -> 196 (M31 crate 14->16, +1 real passing test in the
new programs/riverrun-m31-verifier crate; the memory-wall test stays
#[ignore]d and uncounted, not rounded into 'green').
The open on-chain-verification item was true and stays named, but
'a competitor still leads' is not the right framing for it, on
riverrun's own README, DEFENSE.md, or the PR body (updated to match).
Reworded to 'the one piece still open' everywhere it appeared: same
honest fact, no ranking language handed to anyone else.
… ruled out

Instrumented with real checkpoints (a new
verify_binding_tuned_checkpointed hook, and a heap_used() reader over
the bump allocator's own pointer) rather than guessing further:

- Deserialization + config construction together use only 16,196 of
  262,144 heap bytes (under 10%) for a 4-query BindingProof, all the
  way up to the verify() call. verify() itself is what needs the
  remaining 240KB+.
- Added a second on-chain instruction path (discriminator-based) that
  verifies permutation.rs's PreimageProof, the simplest possible AIR
  in this crate (single Poseidon2 block, no vectorization). It ALSO
  exceeds the 256KB ceiling, and consumes MORE CU before failing
  (~3.05M vs ~1.5-2M for BindingProof), ruling out AIR complexity/
  width as the cause: the smallest AIR here fails the same way the
  more complex one does.

Conclusion, stated precisely: the memory wall looks inherent to this
CirclePcs / Keccak-MMCS / FRI verifier construction as configured,
independent of proof size and AIR shape, not a bug in riverrun's own
constraint code. Both #[ignore]d on-chain tests now carry this
diagnosis. 16 tests green (native), 1 passing + 2 documented-ignored
(on-chain).
Tried logging the exact failing allocation's size/alignment from
inside the bump allocator's own failure path, to tell a single giant
allocation apart from cumulative small ones. Reverted: format!/
sol_log themselves allocate, so a failing alloc logging its own
failure can recurse into more failing allocs, which surfaced as
Solana's BPF-to-BPF call-depth limit instead of any useful data.
Documented in place rather than silently dropped, so the next attempt
does not repeat it.
marcelofeitoza/mirror-pool@836b30c (2026-07-28) closed
settle_zk_escrow_is_a_pool_wide_pot_any_leaf_can_spend for real:
program-enforced domain separation on crowd leaves plus a fixed
denomination, verified by inverting their own attack test to assert
the theft is now rejected. Continuing to cite it as an open,
unfixed bug after checking it was closed would be exactly the kind
of stale, discoverable claim this whole document exists to avoid.

The comparison that survives, honestly: riverrun could never have
had that bug class at all (no shared escrow, execute_batch pays
recipients directly), not because we patched it faster. Their
ceremony's deployed keys remain admittedly insecure dev-setup keys,
unchanged as of the same commit; that point stands as-is.
…nd Poseidon2

docs/RELATED_WORK.md already existed with real, substantive
citations (PrivDID, PLUME, Anonymous Self-Credentials, Serjantov &
Danezis, Tutela, prior on-chain STARK verification on Solana) but
was not linked from the README's doc index at all; fixed. Also adds
two citations M31_CIRCLE_STARK.md was missing despite being entirely
about them: Haböck/Levit/Vlasov's Circle STARKs (eprint 2024/278,
what p3-circle implements) and Grassi/Khovratovich/Schofnegger's
Poseidon2 (eprint 2023/323, what riverrun-m31's in-circuit hash is).
…us per module

Every crate and program, one line each, tagged [live] / [tested] /
[blocked] rather than a bare file listing. Compares riverrun-stark
(Winterfell f128) against riverrun-m31 (Plonky3 Circle-STARK)
directly, with real measured numbers for both (1.57M-3.77M CU for
the former, all over Solana's 1.4M/tx cap; the latter's diagnosed
memory wall). Also explains why the default workspace only includes
the four fast crates and excludes the heavy ones, which was
previously implicit in Cargo.toml comments, not documented for a
reader.
Read p3_uni_stark::verifier::verify, CirclePcs::verify, and
p3_fri::verifier::verify_fri's setup directly rather than continuing
to guess from outside. No single oversized allocation stands out in
verify()'s own body. Real signal from the CU data, now explained: cost
stayed nearly flat across a 10x query-count range (4->40 queries,
1.5M->2.06M CU, +37%), which is not the shape a per-query-dominated
cost would have. Points at FRI/PCS's one-time setup as the driver,
not the per-query loop.

Named honestly where this stops: pinpointing the exact allocation
needs instrumenting or forking p3-fri/p3-circle internals directly,
materially bigger and riskier than the two toolchain patches already
in this crate (a build flag and a stable-API rewrite vs editing real
verification logic in an upstream crypto library). Not attempted
without separate sign-off.
…tensibility criterion

The bounty's own judging rubric asks for 'scalable & customizable,
easy to extend with new protocols and interactions.' riverrun-sdk's
act() already has exactly this (Backend and Prover traits, generic
over both the settlement chain and the proof system), demonstrated
three real ways in this repo already (SimBackend/SimProver for
offline tests, the real devnet backend, and two independent real
provers, riverrun-stark and riverrun-m31) -- it was just never
connected explicitly to that criterion anywhere. Added a concrete
section to ARCHITECTURE.md naming exactly what a new protocol or
interaction requires (a new action byte string, and a new Backend
impl only if it settles somewhere not already reached), citing the
real trait signatures and the real files that already exercise them.
…ion gap

Cross-compiled riverrun-core, riverrun-eval, riverrun-sdk, and the
riverrun CLI to x86_64-pc-windows-gnu and confirmed a real, valid
PE32+ binary (file(1)), not claimed from reading the code. No Unix-
specific code blocks the build; the one std::os::unix use in cli.rs
was already correctly #[cfg(unix)]-gated.

That gate revealed a real gap it was masking: on Windows there was no
equivalent to the Unix chmod 0600 on ~/.riverrun/session, which holds
a raw secret. Fixed with a #[cfg(windows)] best-effort icacls call
(ships with Windows, no new dependency) restricting the file to the
current user. Non-fatal on failure, same as the Unix path.

docs/WINDOWS.md states plainly what remains unverified: on-chain
program builds (cargo build-sbf, Anchor) are not confirmed to work
natively on Windows, a known ecosystem-wide Solana-tooling constraint
this repo does not take on solving.
…nosed and closed

Root cause of the 256KB OOM, found by native heap profiling (tracking
allocator + a replay of the on-chain LIFO-bump reclaim semantics): ~92%
of verify()'s 452,760 B peak live heap was p3_uni_stark symbolically
re-evaluating the whole AIR to derive log_num_quotient_chunks — one
compile-time constant for a fixed AIR. Not FRI, not the challenger, not
the allocator (perfect-allocator peak was itself 1.77x the ceiling).

Fix, in the repo's established vendored-patch discipline (third patch,
PATCH.md included, MIT/Apache upstream): verify_with_known_quotient_chunks
takes the constant as a parameter; each AIR pins it (binding, membership,
permutation), each pin guarded by a native test that recomputes it via the
symbolic pass and fails on drift. A wrong pin rejects honest proofs, it
cannot accept forged ones.

Measured on-chain (LiteSVM, tests/cu.rs, now passing, not #[ignore]-broken):
- binding relation, 4-query 15,001 B proof: ACCEPTED, 2,383,973 CU
- tampered public value: rejected (Custom(1))
- preimage at PRODUCTION security (40 queries, 48,749 B): ACCEPTED,
  9,457,190 CU — the heap holds even at full security (109,376 B peak
  live, 171,736 B LIFO watermark, ~90KB margin)

Two further cost cuts on the way there, both measured: keccak256 routed to
Solana's syscall on-chain (SolKeccak256; software keccak cost 6.17M CU) and
opt-level=3 for the SBF build (opt-z cost 4.31M; CU counts instructions).

Named honestly: 2.38M CU is still over the 1.4M single-tx cap, and the
slope is per-query (~348k CU/query, ~70% inside pcs.verify — DEEP column
reduction + MMCS + fold), so the next milestone is per-query cost work
(FRI arity/blowup tuning, column reduction, or staged verification), plus
the buffer-account staging §2 already specifies for production proof size.

20 native tests green; per-phase CU attribution reproducible via the
patch's off-by-default cu-trace feature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GrofpSFKdANkYMF52fDu3
…fuses our own headline

PR solanabr#5 measures the same channel with brackets, sampling intervals and gates
that refuse unpublishable runs. Ours reported a point estimate. This closes
that gap, and the closure costs us our own number.

crates/riverrun-trace/src/uncertainty.rs (new):
- UnresolvedReason / MemberOutcome: why each member went unresolved
  (no origin within bound, budget exhausted, RPC failure), with is_evidence()
  false for all three, since our walk caps at depth 3 / 14 nodes / SOL-only.
- Census + MAX_FAILURE_RATE 1%: a run whose own infrastructure failed too
  often cannot publish. Previously RPC errors were a printed WARNING above
  the number, which is exactly what gets dropped when the number is quoted.
- Bracket: effective-k with unresolved members merged (favourable) and split
  into singletons (adversarial), both exact, so the unresolved fraction is a
  span, not an assumption.
- effective_k_interval: deterministic bootstrap (SplitMix64, seed
  0x726976657272756e, 10,000 replicates), reporting resampling bias beside
  the range rather than hiding a heavy tail inside it.
- The informativeness gate: >=50% resolved and >=8 resolved members, failure
  gate outranking it, refusal carrying the numbers that caused it.

crates/riverrun-trace/src/runs.rs (new): the published Privacy Cash n=30 run
as data, recomputable offline (). A test proves the histogram
transcription is forced by the counts, not chosen.

Measured on our own published run:
  merged 6.45 | split 1.00 | bracket 1.00…6.45
  95% resampling range 4.90…13.31 (bias +1.79)
  gate: REFUSED - 11 of 30 members (37%) resolved, under the 50% floor

README and docs/EFFECTIVE_K.md now lead with 1.0…6.5 and the refusal, credit
PR solanabr#5's rho=0.0955 measurement by name, and state the metric disanalogy
(rho is k-independent and comparable across pools; effective-k is a member
count and is not). Also documents the uncorrected plug-in entropy bias:
every effective-k we report is plausibly optimistic.

Not done, named: the n=30 run's per-member chains were never written to disk,
so runs.rs recomputes arithmetic, not tracing. Closing that needs a --dump
flag plus a live re-run.

143 workspace tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GrofpSFKdANkYMF52fDu3
Adds an off-by-default cu-trace feature chain (riverrun-m31 -> vendored
p3-circle + p3-uni-stark, sol_log_compute_units at phase boundaries) and
uses it to locate the remaining CU gap precisely instead of guessing.

Per FRI query, 4-query binding proof, LiteSVM:
  open_input   296,927 CU   (98%)
  FRI fold       5,285 CU   (2%)
  loop overhead    182 CU

So the ~348k CU/query slope is not FRI folding. open_input is one MMCS
Merkle opening plus a DEEP-quotient dot product over every trace column in
the degree-3 extension field, which makes cost track trace WIDTH. Measured
and now recorded by a test: BindingAir is 316 columns (158 per Poseidon2
block x VECTOR_LEN 2), about 940 CU per column per query.

That names the next lever with a number: the vectorized two-block layout
buys a same-row shared-secret constraint at the price of doubling the width
that open_input pays for. A two-row layout with a cross-row secret equality
would halve open_input, putting a 4-query proof near 900k CU, inside the
1.4M cap. Real work, not attempted here: it needs a row-type selector and
its own soundness tests.

Also measured and rejected: overflow-checks = false cuts 2,384,277 to
2,124,419 CU (11%), reverted, wrapping arithmetic in a verifier is not
worth 11%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GrofpSFKdANkYMF52fDu3
Stellar's Quantum Preparedness Plan (2026) explicitly excludes pairing-based
ZK, states there is no drop-in post-quantum replacement for pairing SNARKs,
and names STARKs as a candidate deferred to future work. riverrun-m31 is that
candidate class, and it is chain-agnostic Rust, so this checks whether it can
actually leave Solana.

It can: `cargo build --target wasm32v1-none --no-default-features` produces
libriverrun_m31.rlib. Three changes, none of them to the relation or the
proof system:

1. `#![cfg_attr(not(test), no_std)]` + `extern crate alloc` at the crate
   root. Every module already ran on core+alloc; only the root pulled std.
2. bincode moved behind a default-on `wire` feature. bincode 1.x depends on
   serde with default features, i.e. serde/std, which makes serde_core drop
   its own no_std and then fail to find a standard library that does not
   exist on a bare-wasm target. It is only the proof wire format, so a host
   with its own serialization turns it off.
3. Nothing else. All six Plonky3 crates in the graph already declare no_std.

Solana is unaffected and re-verified: cargo build-sbf clean, all three
on-chain tests pass (binding ACCEPTED, tampered rejected, production-security
preimage ACCEPTED). 21 native tests green.

What this does NOT claim: no Soroban contract, no instruction-count
measurement, no proof verified on Stellar. It compiles, which is milestone
M1 of vineland's own scoping doc (2026-07-28-soroban-proof-backend-design.md),
and that doc's backend choice picked riverrun-stark on the stated grounds
that riverrun-m31's relation was not built yet. As of 2026-07-29 it is built
and verifies on-chain, so that decision is worth revisiting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GrofpSFKdANkYMF52fDu3
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