skip to content
Skill Issue Dev | Dax the Dev
search
Part of series: relayerless-privacy

Fitting F_RP in 656 bytes on Solana

Print view

Sections

The previous six posts derived F_RP at the level of relations and theorems. This post is the engineering side: every byte and every compute unit.

The headline numbers:

ResourceUsed by F_RPSolana hard capHeadroom
Transaction bytes6561,232 (legacy) / 4,096 (SIMD-0296)576 / 3,440
Compute units~235,0001,400,0001,165,000
On-chain Groth16 verify~150,000(subset of CU above)
Proof size (compressed)128(subset of bytes above)

This is post 8 of 11 in the relayerless-privacy series.

Proof system: Groth16 over BN254

Why Groth16, not PLONK or STARK. Three reasons:

  1. 128-byte compressed proof. Smallest known SNARK output. Critical for Solana’s 1,232-byte transaction envelope.
  2. < 200,000 CU verification on-chain. The sol_alt_bn128_group_op and sol_alt_bn128_pairing syscalls (live since v1.16) make BN254 ops native to the validator runtime.
  3. Existing infrastructure. Light Protocol’s groth16-solana is already deployed; ZK Compression on mainnet uses it.

PLONK is plausible once SIMD-0302 (BN254 G2 arithmetic syscall, in Review as of Q1 2026) activates — but as of writing, full G2 scalar multiplication is not a syscall, so KZG-based PLONK verification is impractical.

STARKs are too big: a single STARK proof is ~50–200 KB, way over the transaction limit. Hybrid wrapping (STARK inner, Groth16 outer) gives the best of both — Theorem 3.8.

ParameterValue
CurveBN254 (alt_bn128)
Proof structureπ = (A ∈ 𝔾_1, B ∈ 𝔾_2, C ∈ 𝔾_1)
Uncompressed size256 bytes (64 + 128 + 64)
Compressed via sol_alt_bn128_compression128 bytes
Security level~128 bits (Barbulescu-Duquesne 2019 conservative estimate)
Trusted setupPer-circuit MPC (Powers-of-Tau)

Hash function: Poseidon over BN254 scalar field

Poseidon is the standard SNARK-friendly hash for BN254 circuits. Solana ships it as a native syscall (sol_poseidon).

ParameterValue
Field𝔽_p where p = BN254 scalar field order
State widtht = 3 (binary tree: 2 inputs → 1 output)
S-box exponentα = 5 (gcd(5, p-1) = 1 holds)
Full roundsR_F = 8
Partial roundsR_P = 57
R1CS constraints per hash8·3·4 + 57·4 = 96 + 228 = 324
Native syscallsol_poseidon (mainnet, v1.16+)

This is what Light Protocol’s compressed-account commitments use. Same hash everywhere keeps the compressed-account ↔ F_RP boundary clean.

Merkle trees

Two trees, both Poseidon-based:

Note commitment tree (depth 32)

ParameterValue
Depthd = 32
Capacity2^32 ≈ 4.3 × 10^9 notes
On-chain state32-byte root in PDA
Off-chain stateLight Protocol ZK Compression (Solana ledger call data)
Membership-proof circuit cost32 · 324 ≈ 10,400 R1CS constraints

Nullifier tree (Indexed Merkle, depth 32)

The nullifier set needs efficient non-membership checks. Sparse Merkle Trees over 254-bit hashes would cost 254 · 324 ≈ 82,300 constraints per non-membership proof. Indexed Merkle Trees (Aztec’s construction) drop this to depth 32:

CIMTnonmem  =  32324+324+256    10,948 R1CS constraints.C_{\mathsf{IMT-nonmem}} \;=\; 32 \cdot 324 + 324 + 256 \;\approx\; 10{,}948 \text{ R1CS constraints.}

A 7.5× reduction at the cost of maintaining a sorted linked list off-chain.

Aztec’s design proves the “low nullifier” — the leaf where the new nullifier would slot in — and asserts the new value is in the gap. Two range checks plus a standard Merkle path.

Pedersen commitments over BN254 𝔾_1

Used for value hiding inside SPST + range-proof aggregation.

ParameterValue
GroupBN254 𝔾_1 (prime order p ≈ 2^254)
GeneratorsG, H ∈ 𝔾_1 with unknown DL relation
CommitmentC = v · G + r · H
Value rangev ∈ [0, 2^64)
Range proofIn-circuit bit decomposition: 128 R1CS constraints / 64-bit value
HomomorphismC_1 + C_2 = Com(v_1 + v_2, r_1 + r_2)

Why BN254 𝔾_1, not Curve25519? Solana’s native Twisted ElGamal commitments live on Ristretto255 / Curve25519. We don’t reuse them for two reasons:

  1. Curve mismatch. Groth16 needs pairing-friendly BN254. Solana’s Ed25519 / Curve25519 is not pairing-friendly. Mixing the two would require expensive cross-curve gadgets.
  2. Different threat model. Token-2022 confidential transfers hide amounts. F_RP needs to hide amounts + senders + receivers + program logic. The two are different protocols on different math; clean separation is correct.

Key derivation

The privacy framework uses its own key hierarchy, independent of the user’s Solana Ed25519 keypair:

KeyDerivationPurpose
Spending key sksk ← {0,1}^256 randomMaster secret
Nullifier key nkPoseidon(sk, "nk")Derives nullifiers
Public key pksk · G (G ∈ BN254 𝔾_1)Identifies note owner
Viewing key vkPoseidon(sk, "vk")Decrypts incoming notes

The Solana Ed25519 keypair signs the transaction envelope (paying the on-chain fee from the privacy program’s reserve). The ZK proof internally proves authorisation via the spending key. Compromise of one does not compromise the other.

Transaction layout

A canonical 2-input / 2-output SPST transaction:

ComponentSize (bytes)Notes
Groth16 proof (compressed)128via sol_alt_bn128_compression
Nullifiers (2 × 32)64Public input; checked against on-chain set
Output commitments (2 × 32)64Poseidon hashes
Merkle root32Anchors the proof to recent state
Fee (u64)8Public, in lamports
Encrypted note ciphertexts (2)128For recipient note discovery
Anchor instruction discriminator8Standard Anchor program
Account references (with ALT)~120Program ID, PDAs, system accounts
Ed25519 signature64Transaction-level auth
Transaction headers~40Recent blockhash, message header
Total~656Within 1,232-byte limit

Headroom: ~576 bytes. Enough for:

  • A second Groth16 proof (composed PPST + SPST).
  • A 4-input / 4-output transaction instead of 2-in / 2-out.
  • Ring signature of size ~17 (instead of 64-byte simple Ed25519 sig) for in-tx anonymity.

Under SIMD-0296 (4,096 bytes), the headroom triples.

Compute unit budget

OperationCU costSource
Groth16 verification (3 pairings + public-input MSMs)~150,000groth16-solana benchmarks
Nullifier set check (2 PDA reads + comparison)~50,000Compressed account lookups
Merkle root validation (1 PDA read)~10,000Light Protocol root cache
Note insertion + state updates (compressed account write via CPI)~20,000ZK Compression v2 batched updates
Borsh deserialization~5,000Standard overhead
Total~235,00016.8% of 1.4M CU limit

Headroom: 1,165,000 CU. Enough for:

  • A second Groth16 verification (composed PPST + SPST): +150K → total 385K CU (27.5% of limit).
  • Auxiliary in-program Poseidon hashing via sol_poseidon for state derivations.
  • CPI calls to external programs (token transfers for unshielding, swap execution for atomic private DEX).

Existing infrastructure used

InfrastructureIntegration pointStatus
Light Protocol / ZK CompressionMerkle tree state, compressed accountsProduction (mainnet)
groth16-solana verifierGroth16 verification crateProduction
sol_poseidon syscallIn-program Poseidon hashingLive (mainnet, v1.16+)
sol_alt_bn128_group_op syscallsBN254 group ops for proof verificationLive (mainnet, v1.16+)
sol_alt_bn128_compressionG1/G2 point compressionLive (mainnet)
Address Lookup TablesCompact account referencesProduction
SIMD-0296 (4,096-byte transactions)Extended tx envelope for ring sigs / PPSTApproved Q4 2025; pending activation

The protocol is deployable today with the legacy 1,232-byte transaction format. SIMD-0296 makes it more comfortable but isn’t a hard prerequisite.

What we still need from Solana

For full F_RP, two SIMDs are nice-to-have:

SIMD-0302 (BN254 G2 arithmetic syscall)

Currently in Review. Adds native G2 scalar multiplication and addition. Without it, full PLONK / KZG verification on-chain is expensive (G2 ops in the BPF VM). With it, F_RP can switch to a universal SRS that doesn’t need a per-circuit Groth16 ceremony.

Estimated impact: PLONK verification ~400–600K CU vs Groth16’s ~150K. Larger but eliminates per-circuit ceremony. Worthwhile tradeoff for a multi-program ecosystem.

Re-activation of the ZK ElGamal Proof Program

Currently disabled following the Phantom Challenge bug (Fiat-Shamir transcript missing a hash input — June 2025). When re-activated, F_RP can lean on the existing native sigma-proof / Bulletproofs verifier for some sub-protocols. Until then, all proofs go through the BN254 Groth16 path.

End-to-end latency budget

For a 2-in / 2-out SPST transaction on commodity hardware:

PhaseTimeNotes
Read on-chain state (Merkle root + recent blockhash)~50 msRPC roundtrip
Local proof generation (Apple M2, 8-core)0.5–1.5 sDominated by FFT + MSM
Transaction broadcast~50 msDirect to validator RPC
Slot inclusion + finality~600 msSolana block time + confirmation
Total user-perceived latency~1.5–3 s

Most of the latency is prover time, not chain time. A GPU prover (ICICLE on RTX 4090) drops this to ~300 ms. Browser-side proving via wasm-bindgen-rayon is workable but slower (~5–8 s) — discussed in Proving in the browser, by the numbers.

What runs on the validators is intentionally boring

On the chain side, F_RP is just three things:

  1. A Solana program (Anchor-based) that verifies Groth16 + nullifier checks + state updates.
  2. A Light Protocol-compatible Merkle tree state.
  3. An on-chain account holding the protocol’s lamport reserve (replenished from shield deposits, drained by validator fee extractions).

That’s it. No relayers, no off-chain operators, no governance multisig (other than for emergency pause). The boring deployment surface is the point.

Bibliography

Previous: UPEE: composing the framework ← · Next: F_RP vs the rest →

Hire me — book a 30-min call $ book →