Circuit 5 — Policy composition (recursion wrapper)
Status: v1a shipping — single-inner recursive wrapper around Circuit 4. v1b (two-inner Circuit 1 + Circuit 2 composition) and v1c (generic N-inner aggregation across all four circuit types) follow in v1.5.
SSCoP principles: meta-circuit wrapping all of the above. CRA provisions: Article 13(1) conformity-assessment evidence retention (one composed proof carries evidence for every mapped provision). DCC scope: enables Level 3 multi-buyer policy composition.
The claim
All four prior circuits verify against the specific compliance policy the buyer has committed to.
Circuit 5 is where Colofon becomes a product rather than a primitive. Each buyer has their own severity thresholds, their own approved-builder allowlists, their own SLAs, their own approved-recipient sets. Circuit 5 lets a single proving run satisfy any number of buyers simultaneously, each verifying against their own committed policy — and emits one compact recursive proof the browser verifier can check in under two seconds regardless of how many inner circuits went in.
This turns enterprise procurement's compliance-policy argument from "you sent us evidence that matches our policy" into "here is one cryptographic receipt; every buyer reads their own policy's claim out of it". The vendor does not re-prove per buyer.
What the proof does not claim — in its v1a form
v1a is a single-inner recursive wrapper around Circuit 4. It proves one inner Circuit 4 proof is valid and passes its seven public inputs through verbatim. It does not yet:
- aggregate multiple Circuit 4 proofs into a single recursive proof (completeness over a customer set)
- compose Circuits 1, 2, 3, and 4 into a single proof with cross-circuit binding enforced recursively
- bind
inner_vk_hashagainst an approved-inner-VK set (v1a runs under the single-trust-domain assumption; see §"Caveats" below)
The full N-inner composition is v1b/v1c work and is described in §"Roadmap" below. v1a's value is that the recursion wiring itself — the Aztec bb_proof_verification library, the bb.js 4.1.3 + noir_js beta.19 pipeline, the outer Barretenberg instance initialised to 2²¹ Grumpkin points — is working end-to-end. That unlocks v1b and v1c with minimal re-work.
Why recursion matters
Without recursion, the bundle is a list of four independent proofs (Circuits 1–4). Verification cost scales linearly: the browser verifier runs four UltraHonk verifications, the bundle size grows with each claim, and adding per-recipient coverage to Circuit 4 adds N more proofs per bundle. For a vendor with 200 customers and an incident across all of them, that is 203 proofs in a bundle — verifiable but fat.
Recursion collapses this. An N-inner recursive proof proves that N inner proofs all verified, without re-running the N inner verifications. The browser verifier runs one verification regardless of N. Bundle size stops scaling with the claim count.
Three places this matters commercially:
- Per-customer completeness for Circuit 4. 200 customers means 200 Circuit 4 proofs aggregated into one recursive proof. Browser verifies in under two seconds.
- Multi-buyer policy composition. Two different buyers with different thresholds mean two separate roots for each buyer-configurable public input (Circuit 3's
cve_tree_root, Circuit 4'sapproved_recipient_root, etc.). One proving run, one recursive proof, satisfies both buyers. - Bundle compactness. A full bundle (Circuits 1–4 + per-customer Circuit 4 batch) compresses from tens of proofs to one, keeping bundle size under 200 KB regardless of customer count.
Construction
The recursion primitive
Circuit 5 uses Aztec's bb_proof_verification Noir library, a thin wrapper around std::verify_proof_with_type with the proving scheme pinned to UltraHonk-with-ZK (PROOF_TYPE_HONK_ZK = 6). The library specifies two type shapes the inner proof must conform to:
| Type | Size | Role |
|---|---|---|
UltraHonkVerificationKey | [Field; 115] | Serialisation of the inner circuit's UltraHonk verification key, produced off-circuit by generateRecursiveProofArtifacts(...).vkAsFields |
UltraHonkZKProof | [Field; 500] | Deflattened inner proof bytes (492 proof fields + 8 rotations) |
The outer circuit calls verify_honk_proof(inner_verification_key, inner_proof, inner_public_inputs, inner_vk_hash). The stdlib does the verification in-circuit and produces a single constraint that the inner proof is valid.
Apertrue's image_aggregator circuit uses the same library for its own recursive composition and confirms the wiring round-trips through the bb.js 4.1.3 + noir_js 1.0.0-beta.19 pipeline. Reuse here is not coincidental — Apertrue and Colofon share the same proving stack, and the recursion pattern is one of the most reusable primitives in the shared infrastructure.
v1a — what actually ships
fn main(
inner_verification_key: UltraHonkVerificationKey, // private witness
inner_proof: UltraHonkZKProof, // private witness
inner_vk_hash: Field, // unbound in v1a
inner_public_inputs: pub [Field; 7], // passed through verbatim
)
The seven public inputs are Circuit 4's seven public inputs unchanged (dkim_pubkey_hash, signed_digest_commitment, sender_domain_hash, send_timestamp_unix, recipient_identity_hash, approved_recipient_root, incident_id_hash). A verifier who trusts Circuit 4's public-input schema sees the same commitments in the same order — the recursive wrapper is semantically transparent at the public-input layer.
Why Circuit 4 first
Circuit 4 has the largest public-input surface among the four (7 Fields) and the most signer-opaque header-parsing structure. Getting the recursion wiring right against Circuit 4 exercises more of the bb_proof_verification surface than Circuit 1, 2, or 3 would — so v1b and v1c inherit a well-tested recursion primitive rather than stressing it for the first time on multi-inner composition.
Roadmap — v1b and v1c
v1b — two-inner composition of Circuits 1 and 2
Circuits 1 and 2 share the approved-builder / authorised-signer identity-commitment pattern. In a bundle they must agree on the signing-identity domain at a public-input level. v1b recursively composes one Circuit 1 proof and one Circuit 2 proof into a single outer proof, with a cross-check that Circuit 1's signer_commitment (pubkey-identity binding over the build pipeline's Fulcio identity) is consistent with Circuit 2's signer_commitment (pubkey-identity binding over the developer's Fulcio identity) only in the shape of the binding, not in the underlying identities themselves. This catches bundles where the build pipeline's SLSA predicate names one commit and the gitsign commit-signature names another.
v1b is the first place inner_vk_hash is bound. Each inner circuit has a distinct VK (Circuit 1 and Circuit 2 are structurally different circuits), so the wrapper asserts each inner VK hash matches the pinned hash for its circuit role. Without this binding, a two-inner composer would accept arbitrary inner circuits — a soundness failure.
v1c — generic N-inner aggregation across all four types
v1c is the full product-shape Circuit 5: aggregate any mixture of Circuits 1, 2, 3, and 4 proofs into a single recursive proof, with all cross-circuit bindings from the bundle verifier collapsed into the composition itself:
- Circuit 1 ↔ Circuit 2: same commit hash, compatible signer commitments
- Circuit 1 ↔ Circuit 3: SBOM root in Circuit 1's SLSA predicate matches Circuit 3's
sbom_root - Circuit 1 ↔ Circuit 4 (per recipient): release identified by Circuit 1's binary digest matches the incident-notification batch's subject release
- Circuit 4 (per recipient) completeness: the set of
recipient_identity_hashvalues across the aggregated proofs equals the committed customer-set root's contents - Circuit 3 / Circuit 4 SLA:
send_timestamp_unix - incident_timestamp <= sla_windowfor every inner Circuit 4 proof
At v1c the bundle verifier becomes a policy reader rather than a cross-check coordinator. The browser reads a single proof; the semantic checks are already collapsed into the recursion. This is the version the whitepaper's §3 Circuit 5 description anticipates; v1a is the wiring milestone on the way there.
Why incremental shipping
Jumping straight to v1c skips two things that matter: end-to-end pipeline confidence (does the bb.js + noir_js + SRS-initialisation chain round-trip a recursive proof in the browser?) and VK binding-discipline (when do you first care about inner_vk_hash?). v1a forces the first question to be answered against the simplest possible wrapper, so a regression there is caught with a 100-line circuit rather than a 1,000-line one. v1b introduces the binding discipline against the smallest legitimate use case. v1c is a straightforward generalisation of v1b once both are stable.
What the v1a proof carries
v1a exposes exactly Circuit 4's public inputs. See Circuit 4's deep-dive for what those commitments carry and what an adversary cannot derive from them. The recursive wrapper does not add or subtract information; it trades linear verification cost for recursive proving cost.
v1c will expose a strictly smaller public-input surface than the sum of inner circuits' public inputs, because cross-circuit bindings collapse into internal equalities that are no longer public. A v1c bundle's public inputs are expected to be: the release binary digest, the buyer-policy commitment hash, the bundle-composition manifest hash, and the set of Rekor tree heads the bundle's signatures anchor to. Everything else is consumed inside the recursion.
Honest caveats
inner_vk_hashis unbound in v1a. Any Field value is accepted. This is a conscious simplification for the single-trust-domain pass-through case: the caller controls both the inner and outer proof, so no adversary surface exists where a different inner VK could be substituted. v1b and v1c bind this hash against an approved-inner-VK Merkle root — a deployment that runs v1a standalone outside the single-trust-domain assumption MUST extend the circuit or pin the expected hash via a downstream policy layer.- Pipeline depends on SRS-initialisation to 2²¹ Grumpkin points. The outer Barretenberg instance requires Grumpkin points for recursive Honk (not BN254 points, which are the default for non-recursive proving). This is initialised at startup via
Barretenberg.initSRSChonk(2**21). Operators running Colofon's hosted prover or a self-hosted prover at scale should confirm the SRS initialisation completes before accepting the first recursive proof request. - Proving time is dominated by the inner proof. v1a adds ~1–2s of outer verification cost on top of Circuit 4's proving time. v1c is expected to add similar overhead per inner proof.
- Browser verification stays fast regardless. The value of recursion is that the verifier side does not grow with the number of inner proofs. v1a browser verification completes in under 2s; v1c should match this.
- v1a does not reduce bundle size. A bundle that ships Circuit 1, 2, 3, 4, and Circuit 5a over a single inner Circuit 4 is larger than the same bundle without Circuit 5a, because the wrapper adds a proof on top of what it wraps. v1c is where bundle-size compaction lands — it replaces all four inner proofs with one recursive proof.
Parameters
| Parameter | Value | Notes |
|---|---|---|
| Inner VK | [Field; 115] | Fixed by bb_proof_verification library |
| Inner proof | [Field; 500] | 492 proof fields + 8 rotations |
PROOF_TYPE_HONK_ZK | 6 | UltraHonk-with-ZK, pinned |
| SRS size | 2²¹ Grumpkin points | Required for recursive Honk |
| bb.js version | 4.1.3 | Pinned in prover + verifier |
| noir_js version | 1.0.0-beta.19 | Pinned across circuits + SDK |
The SRS files are pre-downloaded into the hosted prover's Docker image at build time with pinned SHA-256 hashes — see colofon-prover/Dockerfile for the full hash-pinning apparatus. First proof after a cold start is immediate; no CDN-fetch runtime dependency.
Further reading
- Whitepaper §3 Circuit 5 — Whitepaper
- Source circuit (v1a) —
colofonhq/colofon-circuits/policy_composition/src/main.nr - Aztec
bb_proof_verificationlibrary — upstream reference for the recursion primitive - Apertrue
image_aggregator— sibling project using the same recursion pattern; round-trip-confirmed through bb.js 4.1.3 - Circuit 4 (v1a's inner circuit) — Circuit 4
- Circuits 1, 2, 3 (v1b/v1c inner circuits) — Circuit 1, Circuit 2, Circuit 3