UltraHonk backend: real proving with Barretenberg
How crucible-prover generates and verifies real UltraHonk proofs through
the Barretenberg binary (bb), and what this repository has validated about
that pairing.
Toolchain pairing (validated)
| Component | Version | Role |
|---|---|---|
nargo | 1.0.0-beta.26 | compile circuits, solve witnesses |
bb | 6.0.0-nightly.20260903 | UltraHonk prove / verify |
This exact pairing is what the repository is validated against: the
compatibility matrix in crates/ultrahonk/src/backend.rs (BACKEND_COMPAT),
the TESTED_BB_VERSION pin in the crate root, and the CI circuits job (which
installs bb via bbup at that version) all agree on it. Proofs are only
reproducible when the backend version is known: an UltraHonk proof produced
by one Barretenberg version may not verify with another.
Installing
bb:bbupnormally resolves the rightbbfor yournargoautomatically, but its mapping (bb-versions.json) can lag new Noir releases. When that happens, pin explicitly:curl -L https://raw.githubusercontent.com/AztecProtocol/aztec-packages/refs/heads/next/barretenberg/bbup/install | bash bbup -v 6.0.0-nightly.20260903 --no-modify-path
scripts/check-bb.shreports the installed version and repeats these instructions.
The tool split (why bb exists at all)
Modern Noir (1.0.0-beta.x) removed proving from nargo:
| Stage | Tool | Produces |
|---|---|---|
| Compile | nargo compile | ACIR bytecode JSON |
| Execute | nargo execute | solved witness (.gz) from Prover.toml |
| Prove | bb prove | UltraHonk proof + verification key |
| Verify | bb verify | accept/reject against the VK |
nargo test output is an in-process interpreter run and is not a proof;
only bb produces cryptographic evidence.
The bb CLI surface (what this adapter drives)
The validated bb exposes a small, stable command surface (no more
prove_ultra_honk / write_vk_ultra_honk subcommands):
bb prove -b <bytecode.json> -w <witness.gz> -o <dir> --write_vk --output_format json
bb verify -p <dir>/proof.json -k <dir>/vk.json -i <dir>/public_inputs.json
- The scheme for Noir ACIR is
ultra_honk. - With
--output_format json, every artifact is self-describing and carriesscheme,bb_version, and the backend-native verification-key digest (vk_hash/hash):proof.json—{ proof: [field words…], vk_hash, bb_version, scheme }public_inputs.json—{ public_inputs: [field words…], bb_version, scheme }vk.json—{ vk: [field words…], hash, bb_version, scheme }
- Field words are
0x-prefixed 32-byte big-endian hex. Public inputs are listed in circuit order:pubparameters first, then returned values. bb verifyexits 0 on acceptance and non-zero on rejection; a rejected proof prints e.g.UltraVerifier: verification failed at reduction step.
Repository mapping
crates/ultrahonk/src/toolchain.rs—BbToolchain: locatingbb(BB_BINoverride), parsingbb --version, major-version floor (pre-2026 CLI generations are rejected).crates/ultrahonk/src/exec.rs—prove()/verify(): process execution, JSON artifact parsing, provenance validation (scheme must beultra_honk,bb_versionmust be present, and the digest a proof embeds must equal the digest of the VK written alongside it). A proof that fails verification is an outcome, not an error.crates/ultrahonk/src/store.rs— the filesystem verification-key store (VkStore): providers write thevk.jsona proof was produced with and verifiers resolve it by id underuhk/<circuit>/<version>/<artifact-sha>.crates/ultrahonk/src/provider.rs—UltraHonkProviderimplementing [ProofProvider]: request bags →Prover.toml(witness encoder, 0600, in a scratch package copy) →nargo execute→bb prove→ a [ProofResponse] whose public outputs are named from the pinned circuit surface.crates/ultrahonk/src/verifier.rs—UltraHonkVerifierimplementing [Verifier]: resolves the key by id, re-encodes the submitted public outputs, and letsbb verifydecide; precise context rejections are detected before cryptography runs.crates/ultrahonk/src/backend.rs— the compatibility matrix.crates/ultrahonk/src/calldata.rs— public-input encoding for an on-chain verifier (candidate layout; calibration against a deployed Soroban verifier is deferred to the Soroban batch).
Witness and bytecode are referenced by path only on the exec layer;
private values leave memory once, through the witness encoder into a 0600
scratch Prover.toml. Errors carry paths and exit codes, never values or
raw stderr.
Live test coverage
tests/tests/ultrahonk.rs runs real cryptography end to end, gated on both
nargo and bb being on PATH:
- witnesses solved from the committed vector catalog against the real
register/transfercircuit packages; bb prove→bb verifyround trips;- binding: a register proof’s single public input must equal the
committed account address; a transfer proof exposes exactly nine public
words — token, sender, recipient, old commitment,
root_hi,root_lo, then the three returned values — checked word by word against the fixture; - rejection: tampered proofs, tampered/wrong verification keys, and proofs submitted against changed public inputs all fail verification.
These are the cryptographic counterparts of the wrong-context rejections the mock backend can only simulate, and they run in the CI circuits job where the validated toolchain pair is installed.
Trait-level wiring and state binding
UltraHonkProvider/UltraHonkVerifier implement the same
[ProofProvider]/[Verifier] seams crucible-mock implements, so
simulators and scenario runners swap backends without changing code. The
VkStore is the resolution layer: a proof never carries key material —
verifiers resolve it by the id stamped on the response.
For the state-bound operations (merge, transfer, withdraw) the state
reference is now a cryptographic binding: the circuits fold the two
halves of the state root into their public inputs and into every emitted
nullifier (see docs/circuit-model.md). A proof cut for root A therefore
embeds different public words than one cut for root B.
UltraHonkVerifier enforces the binding at two layers:
- Structural — before
bbruns, the submitted state reference must agree with theroot_hi/root_lowords the proof committed to; a stale submission is rejected withStateReferenceMismatch, one stripped of its binding withMissingStateBinding. - Cryptographic — if the submitter rewrites both the state reference
and the root words to root B,
bbrejects: the proof was cut for root A.
tests/tests/real_backend.rs exercises both layers, plus the honest
counterpart: register proofs remain deliberately unbound (no state is
consumed) and deposit carries only an envelope-level reference — each
pinned by regression tests so the boundary cannot silently widen.
The trait-seam suite needs nargo + bb on PATH and compiled bytecode
under circuits/target/ (CI compiles before running it).
What is deliberately not here yet
- On-chain verification (Soroban UltraHonk verifier, calldata calibration,
verifier_targetselection) is the Soroban adapter’s job. - The circuits in this repository encode the shape of Confidential Token semantics; exact commitment layout and key derivation must be aligned with the real OpenZeppelin spec before any production use.