Privacy Layer
Groth16 over BLS12-381, Poseidon, in-circuit u64 range proofs, replay-protected withdrawals.
Privacy layer
Paraloom's shielded pool uses Groth16 zk-SNARKs over BLS12-381 with a Poseidon-hashed sparse merkle tree. Three transaction types — deposit, transfer, withdraw — give a Sapling-shaped lifecycle: SOL enters the pool publicly, moves privately inside it, and leaves to a recipient address that's unlinkable to any input.
Primitives
Poseidon hash
zk-SNARK-friendly algebraic hash. Used for commitments, nullifiers, and merkle path nodes.
| Parameter | Value |
|---|---|
| Curve / field | BLS12-381 scalar field |
| Full rounds | 8 |
| Partial rounds | 57 |
| Alpha | 5 |
| Security level | 128 bits |
| Cost in-circuit | ~500 constraints / hash |
Pedersen commitments
Used for value commitments where additive homomorphism matters (compute layer). Implementation: src/privacy/pedersen.rs.
Groth16 zk-SNARK
| Value | |
|---|---|
| Curve | BLS12-381 |
| Proof size | 192 bytes |
| Verify | ~10 ms (single CPU core) |
| Generate | ~2–3 s |
| Setup | trusted (BGM17 phase-2 MPC ceremony) |
| Batch verify | yes — amortizes pairings across a batch |
Batch verification lives in src/privacy/batch.rs.
Sparse merkle tree
Pool commitments accumulate in a sparse merkle tree backed by RocksDB. Path verification is a fixed-depth circuit constraint sequence.
Three transaction types
Commitment
c = Poseidon(value || randomness || owner_pk)Nullifier
n = Poseidon(c || secret)A nullifier reveals only that some commitment was spent — not which one. The on-chain program inits a per-nullifier PDA on withdraw, so a second attempt with the same nullifier fails at the chain level even if validators were tricked.
Withdrawal circuit
Public inputs (revealed on-chain)
pub struct WithdrawPublicInputs {
nullifier: [u8; 32],
amount: u64,
recipient: [u8; 32],
merkle_root: [u8; 32],
expiration_slot: u64, // replay protection
}Private inputs (witnessed)
struct WithdrawPrivateInputs {
value: u64,
randomness: [u8; 32],
secret: [u8; 32],
merkle_path: Vec<[u8; 32]>,
}Constraints (paraphrased)
// 1. Recompute commitment from witness — Poseidon, ~500 constraints
let c = Poseidon(value || randomness || owner_pk);
// 2. Verify merkle path to public root
assert_eq!(merkle_root, walk(c, merkle_path));
// 3. Recompute nullifier and bind to public input
let n = Poseidon(c || secret);
assert_eq!(nullifier, n);
// 4. Range proof: value fits in u64
// bit-decompose value into 64 bits, assert each bit ∈ {0,1},
// assert reconstruction == value
range_check_u64(value);
// 5. Withdrawal amount cannot exceed input value
assert!(value >= amount);The u64 range proof is in-circuit — soundness rests on Groth16, not on validator math. This closed #60; without it, a malicious prover could forge an input that wraps around the field arithmetic and create value out of nothing. Total circuit size remains compact — full breakdown in src/privacy/circuits/withdraw.rs.
Replay protection
Each withdrawal carries an expiration_slot baked into the proof's public inputs. The on-chain program rejects the tx if current_slot > expiration_slot. Combined with the per-nullifier PDA, this prevents:
- Replaying a captured withdrawal off-chain or on-chain
- Long-tail replay attacks where a leaked proof becomes valid again
- Race conditions where coordinator state and chain state drift
Mechanism added in #61.
Proving keys
Devnet keys are generated locally:
cargo run --release --bin setup_withdrawal_ceremony
# → keys/devnet/{deposit,transfer,withdraw}_pk.bin
# → keys/devnet/{deposit,transfer,withdraw}_vk.binMainnet keys come from the multi-party BGM17 phase-2 ceremony — see Ceremony. Mainnet is gated on at least one fully-verified ceremony transcript.
Proof codec
src/privacy/codec.rs handles serialization of proofs and public inputs to/from compact byte representations suitable for gossip and on-chain submission.
What privacy gives, and where it stops
| Hidden | Visible | |
|---|---|---|
| Deposit | recipient inside pool | sender, amount on-chain |
| Transfer | sender, recipient, amount | nullifiers (set membership only) |
| Withdraw | which deposit funded it | recipient, amount on-chain |
Crossing the pool boundary in either direction is necessarily public — privacy is an off-chain property, anchored by on-chain settlement.
Comparison to Zcash Sapling
| Sapling | Paraloom | |
|---|---|---|
| Curve | BLS12-381 | BLS12-381 |
| Proof system | Groth16 | Groth16 |
| Hash | Poseidon (8/57) | Poseidon (8/57) |
| Proof size | 192 B | 192 B |
| Settlement | Zcash chain (PoW) | Solana (Anchor program) |
| Verifier | full nodes | 7/10 BFT cohort |
| Replay protection | tx version + nullifier set | nullifier PDA + expiration_slot |
References
- Circuits:
src/privacy/circuits/ - Pedersen + ownership proof:
src/privacy/pedersen.rs - Codec + batch verification:
src/privacy/codec.rs,batch.rs - Sparse merkle tree:
src/privacy/merkle.rs