Artifact Management
The proving path must always know exactly which circuit produced this
proof. crucible-prover answers that with a pinned artifact root: every
compiled circuit sits in the repository next to a manifest declaring each
file’s SHA-256, and no component proves against ad-hoc bytecode.
See security.md G4 and threat-model A4 (the artifact
swapper) for the adversarial framing.
Layout
artifacts/
└── circuits/
├── register/
│ ├── manifest.json # declares files + SHA-256s, backend, versions
│ └── register.json # compiled ACIR bytecode
├── deposit/ …
├── merge/ …
├── transfer/ …
└── withdraw/ …
Verification keys produced during proving are written to
artifacts/verification-keys/ by the VkStore, keyed by a digest of
circuit id + circuit version + artifact checksum — never by an untrusted
proof-supplied name.
The manifest
Each manifest.json records the circuit identity (circuit,
circuit_version, artifact_version), the backend it was built for, and
one entry per file:
{
"manifest_version": 1,
"circuit": "transfer",
"circuit_version": "0.1.0",
"artifact_version": "0.1.0",
"backend": "ultrahonk",
"files": [
{ "path": "transfer.json", "sha256": "7cf88c43…", "kind": "acir" }
],
"backend_metadata": { "generated_by": "crucible-prover/0.1.0" }
}
Generation is deterministic: identical bytecode reproduces byte-identical manifests, which is what makes the CI freshness gate (below) possible.
The provider gate
UltraHonkProvider::generate refuses to prove until the artifact passes
crucible-artifacts’ strict loader:
- the manifest must parse and its paths must stay inside the artifact directory (no traversal);
- every declared file must exist and match its SHA-256 byte-for-byte;
- no undeclared file may be present (strict mode).
Only then is a witness solved and bb invoked. Failures map to
ProviderError::ArtifactIntegrity (tampered bytes, extra files) or
ArtifactUnavailable (missing manifest/bytecode) — always before any
proving work, so a swapped artifact is rejected without touching secrets or
backend state.
CLI
crucible-prover artifacts check # verify all five pinned artifacts
crucible-prover artifacts generate # re-pin from circuits/target (deterministic)
crucible-prover artifacts generate transfer # re-pin one op
checkruns the same strict loader the provider runs and exits non-zero listing every problem.generatecopies<circuits>/target/<op>.jsoninto the pinned root next to a freshly computed manifest. Requires compiled bytecode (crucible-prover circuits compile).
Keeping artifacts honest
A circuit-source change that forgets to re-pin its artifact is a stale-bytecode hazard: witnesses would be solved against new source while proofs verify against old circuits. CI therefore enforces two gates:
artifacts check— the committed artifacts are self-consistent (manifest matches files);- a fresh-compile determinism gate —
artifacts generateinto a temp root, thendiff -ragainst the committedartifacts/circuits. Any circuit change that didn’t re-pin fails the build.
The toolchain is pinned to match the committed artifacts
(noirup -v 1.0.0-beta.26, bbup -v 6.0.0-nightly.20260903) so fresh
compiles are reproducible.
Tested by
- the live
tests/tests/artifacts.rssuite — attacks a copy of the pinned artifact: single-byte bytecode flip, missing manifest, missing bytecode, planted extra file; all must be rejected before proving, and an intact pinned register artifact must still prove a real UltraHonk proof; tests/security/artifact_tampering.rs(mock-tier) and thecrucible-artifactsunit suite (traversal manifests, checksum edits).