ParaloomPARALOOM

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.

ParameterValue
Curve / fieldBLS12-381 scalar field
Full rounds8
Partial rounds57
Alpha5
Security level128 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
CurveBLS12-381
Proof size192 bytes
Verify~10 ms (single CPU core)
Generate~2–3 s
Setuptrusted (BGM17 phase-2 MPC ceremony)
Batch verifyyes — 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.bin

Mainnet 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

HiddenVisible
Depositrecipient inside poolsender, amount on-chain
Transfersender, recipient, amountnullifiers (set membership only)
Withdrawwhich deposit funded itrecipient, 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

SaplingParaloom
CurveBLS12-381BLS12-381
Proof systemGroth16Groth16
HashPoseidon (8/57)Poseidon (8/57)
Proof size192 B192 B
SettlementZcash chain (PoW)Solana (Anchor program)
Verifierfull nodes7/10 BFT cohort
Replay protectiontx version + nullifier setnullifier PDA + expiration_slot

References

On this page