Introduction
Zero-knowledge proofs let a program accept a short certificate in place of re-executing an expensive computation. On Solana, though, the verifier side has been a bottleneck: until now the ecosystem shipped exactly one production-grade ZK verifier, Light Protocol's groth16-solana.
Every other system, PLONK, HyperPlonk, Halo2-KZG, FRI-STARK, Risc0, Nova, ProtoStar, had to be awkwardly wrapped inside a Groth16 proof (adding a recursion step, a second trusted setup, and latency) or simply could not be verified on Solana L1 at all.
Mosaic removes that constraint. It exposes a single ProofSystem trait over a uniform byte-slice API, so you select a proving system through a generic parameter and swap systems without changing a line of program logic. The library is deliberately thin: it is verification plumbing, not new cryptography, and delegates every field and curve operation to ark-bn254 or audited Solana syscalls.
If you are building a rollup, a privacy protocol, an identity or attestation system, or any dApp that settles a proof on-chain, Mosaic is the verifier layer underneath it.
Installation
Add the core crate plus the crate for the proving system you intend to use. The solana feature switches the curve backend from arkworks (host) to Solana syscalls (on-chain), enable it for any crate compiled to the SBF target.
[dependencies]
mosaic-core = { version = "0.1", features = ["solana"] }
mosaic-groth16 = { version = "0.1", features = ["solana"] }
solana-program = "2.1"
# Add only the systems you need:
# mosaic-plonk = { version = "0.1", features = ["solana"] }
# mosaic-nova = { version = "0.1", features = ["solana"] }Mosaic's minimum supported Rust version is 1.85.0 on the host and 1.89.0-dev for the Solana SBF target. Pin your toolchain accordingly so host tests and on-chain builds stay in lock-step.
Quick start
Start off-chain. The host backend runs the verifier with arkworks, which is the fastest way to confirm your verifying key, proof and public inputs line up before you ever touch a validator.
use mosaic_core::{ProofSystem, ProofSystemId};
use mosaic_groth16::Groth16Verifier;
let backend = mosaic_core::syscall::host::HostBackend::new();
let verifier = Groth16Verifier::<_, false>::new(&backend);
verifier.verify(&vk_bytes, &proof_bytes, &public_inputs_bytes)?;The const-generic false selects little-endian public inputs; the verifier takes three byte slices and returns Ok(()) on success. Now move the exact same verifier into a Solana instruction handler, only the backend changes:
use mosaic_core::syscall::solana::SolanaSyscallBackend;
use mosaic_groth16::Groth16Verifier;
pub fn process_instruction(
_program_id: &Pubkey,
_accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let backend = SolanaSyscallBackend::new();
let verifier = Groth16Verifier::<_, false>::new(&backend);
let (vk, rest) = decode_lp(instruction_data)?;
let (proof, pi_bytes) = decode_lp(rest)?;
verifier.verify(vk, proof, pi_bytes).map_err(Into::into)
}decode_lp reads a length-prefixed segment from the instruction data, so the handler stays agnostic to how the client packed the verifying key, proof and inputs. Because the host and on-chain paths share one SyscallBackend trait, anything that passes in CI passes on-chain too.
Proof systems
Groth16 and KZG-PLONK are production-ready today. Four more families ship with complete structural scaffolds and estimated compute-unit budgets, the trait, dispatch and byte layout are in place while production hardening and audit proceed.
| System | Family | Status | On-chain CU |
|---|---|---|---|
| Groth16 | Pairing · BN254 | Production | 83,574 |
| Groth16 Batch | Pairing · BN254 · N=5 | Production | 258,397 |
| KZG-PLONK | Universal · BN254 | Production | 968,457 |
| HyperPlonk | Universal · BN254 | Phase-3 | ~505K |
| Halo2 KZG | Universal · BN254 | Phase-3 | ~580K |
| FRI-STARK | Transparent · Hash-based | Phase-3 | ~9.4M |
| Nova | Folding · IVC | Phase-3 | ~885K |
Selecting a system is a type-level choice. Swapping Groth16Verifier for PlonkVerifier changes which crate you import and nothing else, the verify call site, the instruction handler and the client are identical.
Verifying a proof
Every verifier implements the same contract: take the verifying key, the proof and the public inputs as byte slices, and return Ok(()) on success or a typed error on failure. Keeping the surface at the byte level is what makes systems interchangeable.
Byte layout & forward compatibility
The serialized format is versioned by a LE_INPUTS const generic (endianness of public inputs) and a FormatTag wire enum. New proof systems extend the wire format without breaking existing consumers, so a verifying key serialized today still decodes after future upgrades.
Proofs from snarkjs
Proofs and keys produced with snarkjs decode straight into canonical bytes, and a local pre-flight runs the verifier with arkworks so you fail fast, before paying for a transaction that would revert.
use mosaic_serde::snarkjs::SnarkjsCodec;
let bundle = SnarkjsCodec::decode_bundle(
&proof_json,
&vk_json,
&public_inputs_json,
)?;
// bundle.vk, bundle.proof, bundle.public_inputs, canonical bytes
mosaic_sdk::preflight(&request)?; // runs the verifier locally; fails fastError model
Mosaic uses a two-layer error model. On-chain, the verifier returns a deterministic OnChainError that maps to a stable program error code and keeps compute cheap. Off-chain, tooling gets a rich DiagnosticError with context about exactly which check failed, so developers stay informed while the chain stays lean.
From circuit to pairing
Under the hood, a zero-knowledge proof travels from an arithmetic circuit down to a single pairing equation that Mosaic checks on-chain. Scroll the chain to follow each step.
- 01
Arithmetic circuit
The statement is expressed as gates over a finite field, with every wire holding a field element.
- 02
R1CS
Each gate becomes a rank-1 constraint: the product of two linear combinations of the witness z equals a third.
- 03
QAP
Constraints are interpolated into polynomials. A valid witness makes A·B − C divisible by the target polynomial Z(x).
- 04
Groth16 pairing check
Mosaic verifies one pairing equation on-chain. If it holds, the proof is valid, in just 83,574 compute units.
Compute budgets
Solana meters execution in compute units (CU). Request a limit that matches your system before submitting: Groth16 single verification fits comfortably under 100k CU, batch verification of five proofs lands near 258k, and KZG-PLONK needs close to 1M.
use solana_sdk::compute_budget::ComputeBudgetInstruction;
let cu_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000);
let verify_ix = mosaic_sdk::build_verify_proof_ix(&request)?;
transaction.add(&cu_ix);
transaction.add(&verify_ix);The reference program compiles to a 319 KBSBF ELF , only 30.4% of the 1 MB account cap, which leaves ample room to bundle Mosaic alongside the rest of your program's instructions in a single deploy.
Compression
Proof bytes are the dominant cost of getting a proof on-chain, so Mosaic ships compression codecs in the mosaic-chunked crate. They cut bandwidth by 50% for Groth16 and 40–42% for PLONK and Nova, shrinking transaction size and account rent.
Compression is the most security-sensitive surface in the library, so it is guarded by ten dedicated fuzz harnesses: a malformed or adversarial input can never decode into a different valid proof, it can only fail to decode.
Architecture
Mosaic is built to disappear behind your program. Four design rules keep it deterministic on-chain, informative off-chain, and forward-compatible as new systems land:
- Object-safe ProofSystem trait. A single byte-slice API; the dispatcher monomorphizes each system through an exhaustive
match, with no dynamic dispatch on the hot path. - Two-layer error model. Deterministic on-chain errors, rich diagnostics off-chain.
- Syscall abstraction. One
SyscallBackendtrait bridges arkworks host tests and Solana syscalls, so the same verifier runs unchanged in both. - Forward-compatible layout. A
LE_INPUTSconst generic andFormatTagwire enum version the byte format.
The workspace splits cleanly by responsibility, core trait, one crate per proof family, serialization, compression, the reference program, client SDK, benchmarks and fuzzers:
crates/
├── mosaic-core/ # ProofSystem trait + dispatcher
├── mosaic-groth16/ # Groth16 (single + batch)
├── mosaic-plonk/ # KZG-PLONK, HyperPlonk, Halo2
├── mosaic-stark/ # FRI-STARK
├── mosaic-nova/ # Nova / HyperNova / ProtoStar
├── mosaic-serde/ # snarkjs + canonical codecs
├── mosaic-chunked/ # compression infrastructure
├── mosaic-program/ # reference Solana program
├── mosaic-sdk/ # client helpers + preflight
├── mosaic-bench/ # criterion benchmarks
└── mosaic-fuzz/ # 37 fuzz targets
docs/ # ADRs, threat model, CU budget
tests/ # differential, integration, fixturesTesting & audit
Correctness is checked differentially: on every run, the on-chain output is compared byte-for-byte against an arkworks reference implementation. If the two ever disagree, the test fails.
- 549+ library tests across 12 crates
- 152+ proptest sessions and 549+ differential tests
- 37 fuzz targets, including 10 dedicated compression harnesses
Phase-1 and Phase-2 components are frozen at production quality; Phase-3 families are scaffolded and estimated. The library is ready for review and an external audit is pending commissioning , track status and read the threat model in the repository.