Witness model
How a proof request’s inputs are split, assembled, validated, and encoded for the toolchain — and why the private/public boundary is structural, not a convention.
The boundary
Every operation circuit has two kinds of inputs:
PRIVATE WITNESS values only the prover knows (secrets, amounts, blindings)
PUBLIC INPUT context everyone can see (addresses, commitments, state root)
In code the split is enforced by two bag types that cannot be confused:
PrivateWitnessBagholdsSecretValues.SecretValueimplements noDebug, noDisplay, noSerialize— it cannot be formatted, logged, or serialized by accident. The bag exposes names and counts only.PublicInputBagholdsFieldValues (canonical hex), which are public by design and safe to log and fixture.
A request carries both sides plus the circuit identity it belongs to
(interfaces::ProofRequest). Because the private bag cannot serialize,
ProofRequest has no Serialize either; its only JSON path is the redacted
view ([ProofRequest::redacted]).
Assembly
crucible-witness owns the seam between a request and the toolchain:
- [
builder] —WitnessAssemblermerges public and private bags into oneWitnessData, rejecting name overlap between the two sides and missing required names. Required names are caller-supplied because the exact circuit interface is defined per circuit (interfaces::circuit:: expectationspins the per-operation public surface). - [
validation] — structural rules every witness must satisfy before it can be encoded. - [
decoder] — parses circuit public outputs back into anOutputBag. It never reconstructs private values.
Encoding (the single escape hatch)
Private values leave memory in exactly one place:
[encoder::write_prover_toml], which writes the Noir Prover.toml layout
as 0x-prefixed hex with 0600 permissions (Unix). Two rules make this
safe:
- Nothing else in the workspace prints a secret value:
WitnessDataand both bags implementDebugas redacted views, error messages carry paths and counts, and toolchain stderr is never echoed into errors (compiler diagnostics can embed source snippets). 0x-prefixing is not cosmetic: Noir’s witness parser treats a bare string as decimal, so an unprefixedabfails to parse and a bare1234silently means decimal 1234. The encoder guarantees hex.
The assembly step is also exposed directly — crucible-prover witness build builds a witness from a test vector and either prints a redacted
summary or writes a Prover.toml for hand-off to a toolchain.
Where this fits
ProofRequest ──► WitnessAssembler ──► WitnessData
│ encoder (0600)
▼
Prover.toml ──► nargo execute (witness solve)
│
▼
bb prove (consumes witness + pinned bytecode)
crucible-vectors maps catalog JSON onto the same bags so fixtures
exercise the identical code path as live proving. See
docs/proving-model.md for the request/response model and
docs/privacy.md for the guarantees that follow from this
design.