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.
src/: verifier generator CLI and templatessolana_verifier/: reference on-chain verifier runtimedata_writer/: generic helper program for packed proof/public-input uploadstest_client/: local-validator driver for staged verificationtest_circuit/: small example Noir circuitfull_verifier.py: host-side Python reference verifier
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.
- Rust
- Solana CLI with
solana-test-validator,solana program deploy, andcargo-build-sbf - Noir toolchain:
nargo - Barretenberg CLI:
bb
For Solana verifier parity:
--oracle_hash keccak--init_kzg_accumulator
If these flags are omitted, bb artifacts and the generated Solana verifier can diverge.
cargo build --release
cargo-build-sbf --manifest-path data_writer/Cargo.tomlOptional: install the generator binary globally.
cargo install --path .The installed command name is:
noir-verifier-generatorWithout install, use:
cargo run -- <subcommand>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
For the bundled circuit:
python3 full_verifier.py test_circuit/target/proof/proof test_circuit/target/vk/vk 30This is the reference host verifier kept in the repo.
Using cargo run:
cargo run -- generate \
-k test_circuit/target/vk/vk \
-o generated/test_circuit_verifier \
-n noir_verifierUsing the installed binary:
noir-verifier-generator generate \
-k test_circuit/target/vk/vk \
-o generated/test_circuit_verifier \
-n noir_verifierThis 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.
cargo-build-sbf --manifest-path generated/test_circuit_verifier/Cargo.tomlExample:
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.1In another shell, fund the deploy key:
solana airdrop 100 --url http://127.0.0.1:8899 --keypair ~/.config/solana/id.jsonDeploy 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.soDeploy 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.soFetch 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.jsonUse 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.tomlExpected terminal tail:
verification flow passed
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.tomlThen deploy data_writer, deploy <generated_dir>/target/deploy/noir_verifier.so, and run test_client with:
SOLANA_PROOF_PATH=<proof_dir>/proofSOLANA_PUBLIC_INPUTS_PATH=<proof_dir>/public_inputsSOLANA_VK_PATH=<vk_dir>/vkSOLANA_VERIFICATION_PLAN_PATH=<generated_dir>/verification_plan.json
The generator emits a transaction plan automatically.
Current verifier flow shape:
InitSessionContinueSumcheck*AccumulatePublicInputDelta*for public-input delta chunks before the final chunkVerifyRelationsAndPrepareShpleminiwhen the fused step fits, otherwiseVerifyRelationsthenPrepareShpleminiComputeAndVerifyShpleminifor 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
InitSessiontransaction
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_nvalues automatically produce more sumcheck tx - larger
log_nvalues may use separateComputeShpleminiPairingPointsandVerifyShpleministages 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 |
If successful proof verification should unlock business logic, use a staged receipt/session pattern:
- upload proof and public inputs
- run the generated verifier stages until the verifier session is complete
- call your business program
- 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.
- The generated verifier is circuit-specific.
- The generated verifier must be used with the matching
bbVK and proof. data_writeris generic and shared across generated verifiers.test_clientis a working reference harness for local-validator verification.full_verifier.pyis the host-side reference verifier retained in this repo.