Skip to content

Repository files navigation

Noir Verifier Solana

Generate Solana verifier programs for Noir UltraHonk proofs, build them, deploy them, and run staged on-chain verification with automatic transaction planning, compact proofs, compact public inputs, and packed uploads.

For a concrete downstream-integration example, see examples/verified_intent/README.md.

What is in this repo

  • src/: verifier generator CLI and templates
  • solana_verifier/: reference on-chain verifier runtime
  • data_writer/: generic helper program for packed proof/public-input uploads
  • test_client/: local-validator driver for staged verification
  • test_circuit/: small example Noir circuit
  • full_verifier.py: host-side Python reference verifier

Program Layout

The deployed flow uses two on-chain programs:

  • the generated verifier, which is circuit-specific
  • data_writer, which is generic and reused across verifiers

This separation is intentional:

  • the verifier stays focused on proof verification
  • upload logic stays tiny and reusable
  • tooling hides most of the extra wiring by auto-discovering standard deploy keypairs

For end-to-end downstream usage after verification, see examples/verified_intent/README.md.

Requirements

  • Rust
  • Solana CLI with solana-test-validator, solana program deploy, and cargo-build-sbf
  • Noir toolchain: nargo
  • Barretenberg CLI: bb

Non-negotiable proof-generation flags

For Solana verifier parity:

  • --oracle_hash keccak
  • --init_kzg_accumulator

If these flags are omitted, bb artifacts and the generated Solana verifier can diverge.

Production Flow

1. Build the generator and helper programs

cargo build --release
cargo-build-sbf --manifest-path data_writer/Cargo.toml

Optional: install the generator binary globally.

cargo install --path .

The installed command name is:

noir-verifier-generator

Without install, use:

cargo run -- <subcommand>

2. Compile a Noir circuit and generate a proof

Exact example with the bundled test_circuit:

cd test_circuit

nargo compile
nargo execute witness

bb write_vk \
  -s ultra_honk \
  --oracle_hash keccak \
  --init_kzg_accumulator \
  -b ./target/test_circuit.json \
  -o ./target/vk

bb prove \
  -s ultra_honk \
  --oracle_hash keccak \
  --init_kzg_accumulator \
  --verify \
  -b ./target/test_circuit.json \
  -w ./target/witness.gz \
  -o ./target/proof

bb verify \
  -s ultra_honk \
  --oracle_hash keccak \
  --init_kzg_accumulator \
  -k ./target/vk/vk \
  -p ./target/proof/proof \
  -i ./target/proof/public_inputs

cd ..

After this step, the example files are:

  • proof: test_circuit/target/proof/proof
  • public inputs: test_circuit/target/proof/public_inputs
  • verification key: test_circuit/target/vk/vk

3. Optional host-side oracle check

For the bundled circuit:

python3 full_verifier.py test_circuit/target/proof/proof test_circuit/target/vk/vk 30

This is the reference host verifier kept in the repo.

4. Generate a Solana verifier from the VK

Using cargo run:

cargo run -- generate \
  -k test_circuit/target/vk/vk \
  -o generated/test_circuit_verifier \
  -n noir_verifier

Using the installed binary:

noir-verifier-generator generate \
  -k test_circuit/target/vk/vk \
  -o generated/test_circuit_verifier \
  -n noir_verifier

This creates:

  • generated program source
  • embedded VK
  • verification_plan.json

The generated verification_plan.json is the baseline transaction plan the client should execute. It is computed automatically from compact proof size, raw public-input size, and circuit parameters. At runtime, the included clients validate the plan against the actual compacted artifacts and recompute it when the encoded public-input byte length differs from the generated baseline.

The client also compacts artifacts before planning:

  • Honk proofs use the repo's compact proof format. Default pairing points are omitted, and stored G1 commitments are compressed with the BN254 G1 compression syscall-compatible format.
  • Public inputs use a compact varlen field encoding when it is smaller than the raw 32-byte field-element stream. The verifier decodes them back to the original 32-byte field elements before transcript and relation checks.
  • If public inputs are not smaller after encoding, the client keeps the raw byte stream.

5. Build the generated verifier program

cargo-build-sbf --manifest-path generated/test_circuit_verifier/Cargo.toml

6. Start a local validator

Example:

solana-test-validator \
  --reset \
  --ledger validator-ledger \
  --rpc-port 8899 \
  --faucet-port 9900 \
  --gossip-port 8001 \
  --dynamic-port-range 8002-8020 \
  --bind-address 127.0.0.1

In another shell, fund the deploy key:

solana airdrop 100 --url http://127.0.0.1:8899 --keypair ~/.config/solana/id.json

7. Deploy the helper uploader and the generated verifier

Deploy data_writer:

solana program deploy \
  --url http://127.0.0.1:8899 \
  --keypair ~/.config/solana/id.json \
  --program-id data_writer/target/deploy/data_writer-keypair.json \
  data_writer/target/deploy/data_writer.so

Deploy the generated verifier:

solana program deploy \
  --url http://127.0.0.1:8899 \
  --keypair ~/.config/solana/id.json \
  --program-id generated/test_circuit_verifier/target/deploy/noir_verifier-keypair.json \
  generated/test_circuit_verifier/target/deploy/noir_verifier.so

Fetch both program IDs if you want to pass them explicitly:

solana-keygen pubkey data_writer/target/deploy/data_writer-keypair.json
solana-keygen pubkey generated/test_circuit_verifier/target/deploy/noir_verifier-keypair.json

8. Run staged verification on-chain

Use the included client with the generated plan.

SOLANA_DATA_WRITER_ID is optional now: the client auto-discovers it from data_writer/target/deploy/data_writer-keypair.json.

SOLANA_PROGRAM_ID is also optional when SOLANA_VERIFICATION_PLAN_PATH points at a standard generated verifier directory, because the client infers target/deploy/noir_verifier-keypair.json.

SOLANA_RPC_URL=http://127.0.0.1:8899 \
SOLANA_KEYPAIR=~/.config/solana/id.json \
SOLANA_PROOF_PATH=test_circuit/target/proof/proof \
SOLANA_PUBLIC_INPUTS_PATH=test_circuit/target/proof/public_inputs \
SOLANA_VK_PATH=test_circuit/target/vk/vk \
SOLANA_VERIFICATION_PLAN_PATH=generated/test_circuit_verifier/verification_plan.json \
cargo run --manifest-path test_client/Cargo.toml

Expected terminal tail:

verification flow passed

Generic Circuit Flow

Replace the example paths above with your circuit paths:

  • Noir bytecode: <circuit>/target/<package>.json
  • witness: <circuit>/target/witness.gz
  • proof dir: <circuit>/target/proof
  • VK dir: <circuit>/target/vk
  • generated verifier dir: generated/<your_verifier>

The exact pattern is:

nargo compile
nargo execute witness

bb write_vk -s ultra_honk --oracle_hash keccak --init_kzg_accumulator -b <bytecode> -o <vk_dir>
bb prove    -s ultra_honk --oracle_hash keccak --init_kzg_accumulator --verify -b <bytecode> -w <witness> -o <proof_dir>
bb verify   -s ultra_honk --oracle_hash keccak --init_kzg_accumulator -k <vk_dir>/vk -p <proof_dir>/proof -i <proof_dir>/public_inputs

cargo run -- generate -k <vk_dir>/vk -o <generated_dir> -n noir_verifier
cargo-build-sbf --manifest-path <generated_dir>/Cargo.toml

Then deploy data_writer, deploy <generated_dir>/target/deploy/noir_verifier.so, and run test_client with:

  • SOLANA_PROOF_PATH=<proof_dir>/proof
  • SOLANA_PUBLIC_INPUTS_PATH=<proof_dir>/public_inputs
  • SOLANA_VK_PATH=<vk_dir>/vk
  • SOLANA_VERIFICATION_PLAN_PATH=<generated_dir>/verification_plan.json

Automatic Transaction Planning

The generator emits a transaction plan automatically.

Current verifier flow shape:

  1. InitSession
  2. ContinueSumcheck*
  3. AccumulatePublicInputDelta* for public-input delta chunks before the final chunk
  4. VerifyRelationsAndPrepareShplemini when the fused step fits, otherwise VerifyRelations then PrepareShplemini
  5. ComputeAndVerifyShplemini for supported circuit sizes

Uploads are also packed automatically:

  • first upload tx = create_account + first write
  • continuation upload tx = maximally sized writes
  • public inputs are compacted before upload/inline planning
  • public inputs are inlined when their encoded bytes fit in verifier instructions
  • larger encoded public-input payloads can be packed into the proof account when that saves transactions
  • separate public-input upload accounts are used only when inline or proof-account packing is not the cheapest valid shape
  • when the final proof upload chunk fits, it is merged into the InitSession transaction

This means the plan is derived from:

  • log_n
  • compact proof byte size
  • raw public-input byte size for verifier math and compute scheduling
  • encoded public-input byte size for upload and inline packing
  • session-account byte size

So:

  • small proofs do not use the same number of tx as larger proofs
  • small public-input payloads can use zero public-input upload tx
  • compressible large public-input payloads can also use zero public-input upload tx
  • public-input delta usually costs zero standalone tx because the final chunk is folded into the relation-check step
  • incompressible larger public-input payloads automatically fall back to proof-account packing or upload accounts
  • larger log_n values automatically produce more sumcheck tx
  • larger log_n values may use separate ComputeShpleminiPairingPoints and VerifyShplemini stages when the fused final stage would exceed the compute budget

test_client consumes verification_plan.json and validates it against the actual compacted proof/public-input artifacts, so deployers do not need to hand-tune the split.

For the bundled test_circuit, the current generated plan is 8 verifier-flow transactions: 5 proof-account setup/upload transactions, 0 public-input upload transactions, and 3 verifier stage transactions. The checked-in proof is 14,592 raw bytes and compacts to 5,424 bytes. The checked-in public input stream is 32 raw bytes and encodes to 14 bytes.

Latest local-validator matrix:

Case Compact proof bytes Public input raw / encoded bytes Upload tx Verify tx Total tx
bundled small circuit, small public input 5,424 32 / 14 5 3 8
small circuit, 65 public inputs 5,424 2,080 / 142 5 4 9
larger circuit, small public input 6,704 32 / 32 6 4 10
larger circuit, 65 public inputs 6,704 2,080 / 173 7 4 11

Integration Pattern

If successful proof verification should unlock business logic, use a staged receipt/session pattern:

  1. upload proof and public inputs
  2. run the generated verifier stages until the verifier session is complete
  3. call your business program
  4. have the business program check the verifier session and consume its own intent/state exactly once

The complete worked example is in examples/verified_intent/README.md.

Notes

  • The generated verifier is circuit-specific.
  • The generated verifier must be used with the matching bb VK and proof.
  • data_writer is generic and shared across generated verifiers.
  • test_client is a working reference harness for local-validator verification.
  • full_verifier.py is the host-side reference verifier retained in this repo.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages