Fitting F_RP in 656 bytes on Solana
Sections
- Proof system: Groth16 over BN254
- Hash function: Poseidon over BN254 scalar field
- Merkle trees
- Note commitment tree (depth 32)
- Nullifier tree (Indexed Merkle, depth 32)
- Pedersen commitments over BN254 𝔾_1
- Key derivation
- Transaction layout
- Compute unit budget
- Existing infrastructure used
- What we still need from Solana
- SIMD-0302 (BN254 G2 arithmetic syscall)
- Re-activation of the ZK ElGamal Proof Program
- End-to-end latency budget
- What runs on the validators is intentionally boring
- Bibliography
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:
| Resource | Used by F_RP | Solana hard cap | Headroom |
|---|---|---|---|
| Transaction bytes | 656 | 1,232 (legacy) / 4,096 (SIMD-0296) | 576 / 3,440 |
| Compute units | ~235,000 | 1,400,000 | 1,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:
- 128-byte compressed proof. Smallest known SNARK output. Critical for Solana’s 1,232-byte transaction envelope.
< 200,000 CUverification on-chain. Thesol_alt_bn128_group_opandsol_alt_bn128_pairingsyscalls (live since v1.16) make BN254 ops native to the validator runtime.- 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.
| Parameter | Value |
|---|---|
| Curve | BN254 (alt_bn128) |
| Proof structure | π = (A ∈ 𝔾_1, B ∈ 𝔾_2, C ∈ 𝔾_1) |
| Uncompressed size | 256 bytes (64 + 128 + 64) |
Compressed via sol_alt_bn128_compression | 128 bytes |
| Security level | ~128 bits (Barbulescu-Duquesne 2019 conservative estimate) |
| Trusted setup | Per-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).
| Parameter | Value |
|---|---|
| Field | 𝔽_p where p = BN254 scalar field order |
| State width | t = 3 (binary tree: 2 inputs → 1 output) |
| S-box exponent | α = 5 (gcd(5, p-1) = 1 holds) |
| Full rounds | R_F = 8 |
| Partial rounds | R_P = 57 |
| R1CS constraints per hash | 8·3·4 + 57·4 = 96 + 228 = 324 |
| Native syscall | sol_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)
| Parameter | Value |
|---|---|
| Depth | d = 32 |
| Capacity | 2^32 ≈ 4.3 × 10^9 notes |
| On-chain state | 32-byte root in PDA |
| Off-chain state | Light Protocol ZK Compression (Solana ledger call data) |
| Membership-proof circuit cost | 32 · 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:
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.
| Parameter | Value |
|---|---|
| Group | BN254 𝔾_1 (prime order p ≈ 2^254) |
| Generators | G, H ∈ 𝔾_1 with unknown DL relation |
| Commitment | C = v · G + r · H |
| Value range | v ∈ [0, 2^64) |
| Range proof | In-circuit bit decomposition: 128 R1CS constraints / 64-bit value |
| Homomorphism | C_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:
- Curve mismatch. Groth16 needs pairing-friendly BN254. Solana’s Ed25519 / Curve25519 is not pairing-friendly. Mixing the two would require expensive cross-curve gadgets.
- 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:
| Key | Derivation | Purpose |
|---|---|---|
| Spending key sk | sk ← {0,1}^256 random | Master secret |
| Nullifier key nk | Poseidon(sk, "nk") | Derives nullifiers |
| Public key pk | sk · G (G ∈ BN254 𝔾_1) | Identifies note owner |
| Viewing key vk | Poseidon(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:
| Component | Size (bytes) | Notes |
|---|---|---|
| Groth16 proof (compressed) | 128 | via sol_alt_bn128_compression |
| Nullifiers (2 × 32) | 64 | Public input; checked against on-chain set |
| Output commitments (2 × 32) | 64 | Poseidon hashes |
| Merkle root | 32 | Anchors the proof to recent state |
| Fee (u64) | 8 | Public, in lamports |
| Encrypted note ciphertexts (2) | 128 | For recipient note discovery |
| Anchor instruction discriminator | 8 | Standard Anchor program |
| Account references (with ALT) | ~120 | Program ID, PDAs, system accounts |
| Ed25519 signature | 64 | Transaction-level auth |
| Transaction headers | ~40 | Recent blockhash, message header |
| Total | ~656 | Within 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
| Operation | CU cost | Source |
|---|---|---|
| Groth16 verification (3 pairings + public-input MSMs) | ~150,000 | groth16-solana benchmarks |
| Nullifier set check (2 PDA reads + comparison) | ~50,000 | Compressed account lookups |
| Merkle root validation (1 PDA read) | ~10,000 | Light Protocol root cache |
| Note insertion + state updates (compressed account write via CPI) | ~20,000 | ZK Compression v2 batched updates |
| Borsh deserialization | ~5,000 | Standard overhead |
| Total | ~235,000 | 16.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_poseidonfor state derivations. - CPI calls to external programs (token transfers for unshielding, swap execution for atomic private DEX).
Existing infrastructure used
| Infrastructure | Integration point | Status |
|---|---|---|
| Light Protocol / ZK Compression | Merkle tree state, compressed accounts | Production (mainnet) |
groth16-solana verifier | Groth16 verification crate | Production |
sol_poseidon syscall | In-program Poseidon hashing | Live (mainnet, v1.16+) |
sol_alt_bn128_group_op syscalls | BN254 group ops for proof verification | Live (mainnet, v1.16+) |
sol_alt_bn128_compression | G1/G2 point compression | Live (mainnet) |
| Address Lookup Tables | Compact account references | Production |
| SIMD-0296 (4,096-byte transactions) | Extended tx envelope for ring sigs / PPST | Approved 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:
| Phase | Time | Notes |
|---|---|---|
| Read on-chain state (Merkle root + recent blockhash) | ~50 ms | RPC roundtrip |
| Local proof generation (Apple M2, 8-core) | 0.5–1.5 s | Dominated by FFT + MSM |
| Transaction broadcast | ~50 ms | Direct to validator RPC |
| Slot inclusion + finality | ~600 ms | Solana 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:
- A Solana program (Anchor-based) that verifies Groth16 + nullifier checks + state updates.
- A Light Protocol-compatible Merkle tree state.
- 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
- Light Protocol. ZK Compression Whitepaper. https://www.zkcompression.com/resources/whitepaper
- Light Protocol. groth16-solana on-chain verifier. https://github.com/Lightprotocol/groth16-solana
- Helius. Zero-Knowledge Proofs: Applications on Solana. https://www.helius.dev/blog/zero-knowledge-proofs-its-applications-on-solana
- Solana Foundation. Transactions documentation. https://solana.com/docs/core/transactions
- Solana Foundation. SIMD-0296: Larger Transaction Format. https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0296-larger-transactions.md
- Solana Foundation. SIMD-0302 (Review): BN254 G2 Arithmetic Syscalls. https://github.com/solana-foundation/solana-improvement-documents/discussions/293
- Aztec Documentation. Indexed Merkle Tree (Nullifier Tree). https://docs.aztec.network/
- Grassi, L., Khovratovich, D., Rechberger, C., Roy, A., Schofnegger, M. (2021). Poseidon. USENIX Security 2021. https://eprint.iacr.org/2019/458
- Pedersen, T. P. (1991). Non-Interactive and Information-Theoretic Secure Verifiable Secret Sharing. CRYPTO 1991.
Previous: UPEE: composing the framework ← · Next: F_RP vs the rest →