null.
Architecture

Shielded pool internals

The native Groth16 verifier, the incremental Poseidon tree, the nullifier set, the recent-roots window, and the enforced construction rules.

The shielded pool is embedded into the chain executor as a builtin — a transaction whose single instruction targets POOL_PROGRAM_ID (svmchain-shielded-pool-builtin!!) is intercepted by the Node and executed directly and atomically. It is not a deployed program. Shielded lamports are custodied by the POOL_VAULT account.

The circuit proves a transfer is valid; this state machine is what prevents double-spends and value theft. Get the nullifier/commitment bookkeeping wrong and perfect proofs still drain the pool.

The verifier: real, native (no mock)

Proofs are verified with solana-bn254's alt_bn128_pairing_be / alt_bn128_g1_addition_be / alt_bn128_g1_multiplication_be. On a non-Solana target these run the exact arithmetic the sol_alt_bn128_* syscalls dispatch to, so the builtin verifier is bit-identical to what a BPF program would compute — a real verifier, not a shape-checking mock.

  • Encoding: EIP-197 / big-endian. G1 = x‖y (64 B), G2 = x_c1‖x_c0‖y_c1‖y_c0 (128 B), scalars 32-byte BE.
  • Groth16 check: e(-A,B)·e(α,β)·e(vk_x,γ)·e(C,δ) == 1, with vk_x = IC[0] + Σ pub_i·IC[i+1].
  • The verifying key is embedded from the committed verification_key.json (dev-grade — see Trusted setup).
  • Poseidon uses solana-poseidon Bn254X5 (the circom-compatible parameterization the sol_poseidon syscall wraps); a test asserts it equals circomlibjs poseidon([1,2]) and that the empty-tree root matches the client.

BPF verifier path is pending

A verifier deployed as a BPF program would call the same functions, which then dispatch to the real syscalls — that path is not exercised here (litesvm BPF alt_bn128/poseidon syscall availability is unverified). It is on the real-validator debt list. The native builtin path that ships is fully real and tested.

Incremental tree & recent-roots window

Output commitments are appended to an incremental Poseidon(2) tree of depth 20 at monotonically increasing indices. The pool keeps a recent-roots window of N = 30 (Tornado's ROOT_HISTORY_SIZE): a proof built against a root up to 29 insertions stale still verifies while the tree grows underneath it. A recent-but-not-latest root is accepted; an unknown root is rejected.

The construction rules enforced on-chain

The circuit leaves five rules to the contract; the pool enforces each:

  1. Dummy-input nullifiers — the pool cannot tell a dummy (amount == 0) from a real input, so nullifiers are handled uniformly: both nullifiers in a tx must be distinct from each other and absent from the spent-set.
  2. Nullifier uniqueness — an on-chain spent-set; any tx whose input nullifier already exists is rejected (the core double-spend defense).
  3. Commitment uniqueness / append-only — a commitment that already exists is rejected, and the two outputs in a tx must differ.
  4. Shield XOR unshield — shield ⇒ pubIn>0 ∧ pubOut==0; transfer ⇒ pubIn==0 ∧ pubOut==0; unshield ⇒ pubIn==0 ∧ pubOut>0.
  5. Fee bindingfee is a public signal bound by the proof. Shield and transfer require fee == 0; an unshield may carry a fee, routed to the bound relayer (see Relayer).

Recipient / relayer binding (nPublic 12)

The unshield recipient and relayer are bound in the proof, so a third party cannot resubmit a valid unshield with a different destination or steal the fee. The circuit has 12 public signals; the pool reconstructs recipientHi/Lo and relayerHi/Lo from the instruction and Groth16 verification forces them to match — no separate check needed. See Reference → Public signals.

Instruction wire format (505 bytes)

[tag u8]  1=shield 2=transfer 3=unshield
[proof 256]              A(64) ‖ B(128, EIP-197) ‖ C(64)
[root 32]
[nullifier0 32][nullifier1 32]
[outCommitment0 32][outCommitment1 32]
[publicAmountIn u64 LE][publicAmountOut u64 LE][fee u64 LE]
[recipient 32]           (unshield target; zero otherwise — bound in proof)
[relayer 32]             (fee recipient; zero if none — bound in proof)

The Node reconstructs the 12 public signals from these fields (never from a separate client blob) and verifies the proof against them — binding the proof to the exact nullifiers it spends, commitments it inserts, root it checks, amounts it moves, and destination it pays.

Value invariant

POOL_VAULT.lamports == pool_shielded_in_total − pool_shielded_out_total at all times. Shield debits the caller and credits the vault; unshield debits the vault by publicAmountOut + fee and credits the recipient publicAmountOut and the relayer fee; transfer moves nothing external. Nothing enters or leaves the vault without a verified proof.

On this page