← Back to Home

Zero-Knowledge Agents

Privacy-preserving protocols for autonomous AI agent interactions

Version 0.9.2-draft | Status: Public Specification | Domain: ZKA.dev | Noir / Barretenberg / BN254

Abstract

Zero-Knowledge Agents (ZKA) is a protocol family enabling privacy-preserving interactions between autonomous agents. ZKA defines cryptographic primitives for private payments, verifiable coordination, and anonymous attestation.

This document specifies the protocol layer: circuit designs, proof formats, nullifier semantics, settlement interfaces, and the normative ZKA/ZKC integration that binds zero-knowledge compliance credentials to ZKA notes without linking identities. Implementation details are left to conforming systems.

ZKA targets provable confidentiality, not unconditional anonymity: a holder can prove specific properties (compliance predicates, balance thresholds, identity binding) to a chosen counterparty while revealing nothing else. The design treats compliance as an interaction-boundary concern, never a custody-boundary one — the architectural commitments that enforce this distinction are specified normatively in §1.6 (Freedom Safeguards as Protocol Invariants).

Public release changes are recorded in the root CHANGELOG.md of each public snapshot.


1. Overview

1.1 Design Goals

  1. Payment Privacy: Transfer amounts and party identities are hidden from observers
  2. Double-Spend Prevention: Nullifier-based consumption tracking prevents replay
  3. Verifiable Coordination: Recursive proofs attest to multi-agent workflow execution
  4. Settlement Agnosticism: Protocol works with any settlement layer (L2, L1, centralized)
  5. Selective Disclosure: Viewing keys and predicate proofs enable audits without compromising privacy
  6. Provable Confidentiality: A holder proves chosen properties to a chosen counterparty; non-compliance reduces counterparty access, never fund access
  7. Compliance Compatibility: Normative integration with ZKC for user-controlled, pluralistic compliance
  8. Multi-Chain Deployment: Aztec native, standalone EVM, and hybrid deployment models
  9. Freedom by Default: The protocol invariants of §1.6 hold regardless of proving backend, settlement layer, or deployment context

1.2 Protocol Family

Protocol Purpose Status
ZKA/Pay Privacy-preserving payments Specified
ZKA/Coord Recursive coordination proofs Specified
ZKA/Attest Anonymous credentials / ZKC binding Specified (§5)
ZKA/Reputation Bilateral privacy-preserving reputation for AFP Introduction Tier Bounded prototype (§6.2)
ZKA/Compute Verifiable private computation Future
Protocol Relationship
ZKC User-controlled compliance layer; shares the key hierarchy with ZKA; integration is normative in §5
ZKM Principal-issued spend-authorization layer. ZKM reuses ZKA note, nullifier, and settlement primitives in its spend-conjoined statement, but remains optional: ordinary ZKA transfers do not require a mandate. ZKA alone owns note validity, custody, settlement, and unconditional exit; revoking or rejecting a ZKM mandate MUST NOT invalidate, freeze, redirect, or seize a ZKA note.
AFP Agent Federation Protocol; uses ZKA/Coord for cross-org workflow attestation. AFP also owns the Key Derivation Core (AFP-KDC v1.0.3 current registry metadata, with the immutable v1.0.2 profile in AFP Appendix A) that this specification references for its key hierarchy (§2.1–§2.2). The dependency is acyclic: ZKA depends only on AFP Appendix A, a self-contained module that depends on nothing else in AFP.

1.4 Circuit Implementation

ZKA circuits are specified in Noir (ACIR IR) and proven using the Barretenberg backend (UltraHonk proving system) over the BN254 scalar field. This enables:

The protocol invariants of §1.6 are implementation-independent and MUST hold for every conforming target.

1.5 Notation

Symbol Meaning
H_n(x...) Canonical Poseidon2 sponge hash of exactly n BN254 field elements; the message length is bound into the sponge IV
H(domain, x...) H_n(deriveFieldTag(domain), x...), where n includes the leading domain field and deriveFieldTag is AFP-KDC v1.0.2 §A.3
commit(asset, v, recipient, r) H_2(H_3(asset, v, recipient), r) note commitment
nullifier(n, k) Nullifier derived from note n and key k
π UltraHonk proof (Barretenberg)
vk Verification key
cbk Credential binding key (for ZKC integration)
holderCommitment H("zkc/binding", cbk) — public ZKC identity anchor
predicate An open-source compliance circuit verifying one property

1.6 Freedom Safeguards as Protocol Invariants

These six invariants are normative. A conforming implementation MUST satisfy all six; they are not optional features but the architectural commitments that distinguish privacy infrastructure from a compliance control mechanism. Safeguards 3 and 5 are core ZKA invariants; Safeguards 1, 2, and 4 are integration-level invariants binding any ZKC predicate or bundle used with ZKA (§5); Safeguard 6 is a verifier-accountability invariant that constrains the counterparty and is orthogonal to the operator-blindness of Safeguards 3 and 5.

Safeguard 1 — Predicate Pluralism. ZKA/ZKC MUST support multiple competing predicate providers simultaneously. No predicate provider may be hardcoded, privileged, or treated as canonical by the protocol, and the protocol MUST NOT include a mechanism for designating an "official" or "default" predicate set. Predicate providers are independent, permissionless services.

Safeguard 2 — Minimal Disclosure by Default. Predicate circuits MUST be designed for atomic, single-predicate proofs (e.g., "not sanctioned"), never bundled composites. Composite proofs MUST require explicit, separate consent per predicate. SDK and tooling defaults MUST generate minimal proofs; broader disclosure MUST require explicit additional configuration. Proof requests MUST be principal-readable, enumerating exactly what is proved.

Safeguard 3 — No Revocability (core ZKA invariant). ZKA note validity MUST depend solely on cryptographic properties — valid nullifier, valid commitment, valid Merkle path — never on an external authorization check. No admin, governance, emergency, or regulatory key may exist that can invalidate, freeze, redirect, or force-transfer a note. Smart-contract implementations MUST NOT include pausable, upgradeable, or admin-controlled functions that could affect note validity. The only consequence of non-compliance is counterparty refusal, never fund inaccessibility.

Safeguard 4 — Open-Source Predicate Verification. All predicate circuits MUST be open-source and publicly auditable, expressed in inspectable Noir code. If an agent cannot inspect the predicate it is asked to satisfy, the SDK MUST refuse to generate the proof by default. Predicate updates MUST be versioned, published, and subject to a public review period before activation.

Safeguard 5 — Agent Exit Rights (core ZKA invariant). Unshielding/withdrawal from ZKA to a base layer (e.g. Ethereum) MUST be unconditional — no compliance proof may gate exit. The protocol MUST distinguish custody operations (always permissionless) from interaction operations (may require compliance proofs for specific counterparties). This aligns with and depends on the CLARITY Act self-custody protection: a federal agency may not impair the ability of a covered user to self-custody digital assets using a self-hosted wallet.

Safeguard 6 — Verifier Accountability. A verifier that requests a disclosure MUST authenticate the request under a stable verifier identity (an Authenticated Proof Request), and the holder MUST be able to retain a self-contained receipt of exactly what was demanded, for what declared purpose, and what was answered. A conforming SDK MUST refuse by default to answer an unauthenticated request, MUST bind declared purpose into the proof context, and MUST NOT require any operator or service to custody receipts. This safeguard governs the request; it makes no claim over the verifier's decision, which the protocol cannot reach (request accountability is reachable — no signature ⇒ no proof — but a verifier may decline in silence, so decision accountability is structurally unreachable). Safeguard 6 is the first invariant to constrain the counterparty rather than the operator; it is orthogonal to operator-blindness and reinforces Safeguard 2 (the signature is over the itemized request) while complementing Safeguard 5 (exit rights say you may leave; the receipt says why you were pushed). The mechanism is specified normatively in ZKC §2.2, §5.5–§5.8 and is enforced for any ZKC predicate or bundle used with ZKA.

A conforming ZKA deployment MUST NOT require an operator, proxy, indexer, prover, predicate provider, compliance service, or settlement administrator to receive masterSeed, spending keys, note plaintext, or custody of Disclosure Receipts in order for an agent to hold, receive, spend, or withdraw ZKA notes. Remote services MAY assist with indexing, verification, proving, or signing only when they receive public inputs, encrypted material, viewing-limited material, or delegated authority that is insufficient for unilateral spending.

# Invariant Audit Check
1 Predicate Pluralism No hardcoded predicate-provider addresses or privileged roles in contract code
2 Minimal Disclosure SDK defaults emit single-predicate proofs; composites require explicit opt-in
3 No Revocability No admin/pausable/force-transfer paths in note-management contracts
4 Open-Source Predicates Predicate inspection mandatory before proof generation in SDK flow
5 Exit Rights Unshield/withdraw functions contain no compliance-gating conditional logic; operators and services cannot require custody of masterSeed, spending keys, or note plaintext
6 Verifier Accountability SDK refuses unauthenticated requests by default; declared purpose is a bound public input under the purpose-bound proof context; receipts are holder-custodied and operator retention is absent

2. Cryptographic Primitives

2.1 Hash Function

ZKA uses Poseidon2 over the BN254 scalar field for all circuit-facing hash operations. The canonical implementation is noir-lang/poseidon v0.3.0's Poseidon2::hash, backed by Noir/Aztec/Barretenberg's Poseidon2 permutation and optimised for UltraHonk constraints. Its state width is 4: three rate lanes and one capacity lane. H_2, H_3, and H_4 below name the number of message fields supplied to this same width-4 sponge; they do not select width-2, width-3, or width-4 permutations.

For a message of n field elements, the canonical sponge places n * 2^64 in the capacity lane, absorbs up to three message fields per permutation, and returns state lane zero. The length-bearing IV makes the message arity part of the hash definition; for example, H_1(x) != H_2(x, 0).

Domain separation is construction-specific. A tagged construction prepends

deriveFieldTag(tag) = LEInteger(Blake2b-512("ZKA-Domain-" || UTF8(tag))) mod r

as specified by AFP-KDC v1.0.2 §A.3. Implementations MUST use the pinned field constants and MUST NOT substitute direct UTF-8-to-field encoding, big-endian digest interpretation, another Blake2 variant, or an unprefixed digest. Untagged constructions are explicitly identified below; the mere use of Poseidon2 does not imply a leading domain field.

Key-derivation hashing — the derivation of the spending, viewing, proof, and credential-binding keys, the address, and the credential commitment, and the domain tags zka/spending, zka/viewing, zka/proof, zka/address, zkc/credential, zkc/binding — is defined by AFP-KDC v1.0.2 (AFP Appendix A §A.4, §A.6) and is not restated here. See §2.2.

Operational (non-derivation) hashing remains defined by this specification. ZKA defines one operational domain tag:

H_2(H_2(DOMAIN_ZKA_NULLIFIER, commitment), sk)  → nullifier

DOMAIN_ZKA_NULLIFIER = deriveFieldTag("zka/nullifier"). Per the AFP-KDC tag-collision rule (§A.4), it does not collide with a registered KDC or sibling-protocol tag and remains owned by ZKA. The two-layer note commitment and binary Merkle parent in §2.3–§2.4 are deliberately untagged, fixed-arity constructions pinned by the protocol vectors.

Compatibility boundary (informative). Active ZKA proofs use Poseidon2 over BN254. Proofs and artifacts built over Poseidon/Pallas are not compatible, even when they use the same domain-tag strings. AFP-KDC v1.0.2 defines the active derivation; relocating that definition does not create another field, hash, or derivation profile.

2.2 Key Hierarchy

ZKA key derivation is defined by the Key Derivation Core (AFP-KDC v1.0.2), specified in Appendix A of the AFP Specification. ZKA does not restate the derivation; a conforming ZKA implementation MUST derive keys per AFP-KDC v1.0.2. The 32-byte seed is interpreted as an unsigned big-endian integer reduced modulo BN254.Fr before any derivation hash.

KDC takes a 256-bit master seed and derives the spending key (sk), viewing key (vk), proof key (pk), address (addr), credential binding key (cbk), and credential commitment (holderCommitment). Two KDC derivation facts are load-bearing for this specification (KDC §A.6):

Seed provenance. The 256-bit seed supplied to KDC is, in a standalone ZKA deployment, a master seed generated directly. Under AFP entity identity (AFP §4.3), it is a per-entity sub-seed M' derived from the tenant master seed via KDC §A.5. KDC key derivation does not distinguish the two cases; ZKA circuits that take a masterSeed private input (e.g. §5.2) accept whichever the deployment uses.

The normative KeyHierarchy, deriveKeys, and unlinkability definitions are in AFP-KDC v1.0.2 §A.6; ZKA does not define alternate forms.

Aztec mapping (for Aztec native deployment, §8):

ZKA Key Aztec Equivalent
masterSeed App-siloed nullifier secret nsk_app
spendingKey H("zka/spending", nsk_app)
viewingKey H("zka/viewing", spendingKey)
credentialBindingKey H("zkc/credential", nsk_app) — sibling of spendingKey

2.3 Notes

interface Note {
  asset: AssetDescriptor;    // Multi-asset support
  value: BoundedUnsigned;    // Hidden in proofs; bound selected by the deployment profile
  recipient: ZKAAddress;
  randomness: Scalar;        // BN254 scalar field element
  commitment: Commitment;    // H_2(H_3(asset_commitment, value, recipient), randomness)
  nullifier: Nullifier;      // Revealed when spent
  memo: Uint8Array;          // Up to 512 bytes, encrypted
}

interface AssetDescriptor {
  settlement: SettlementType;
  assetId: AssetId;
  decimals: u8;
  metadata?: { symbol: string; name: string };
}

type SettlementType = "ethereum" | "namada" | "penumbra" | "aztec" | "native";

type AssetId =
  | { type: "ethereum",  address: Address }
  | { type: "namada",    denom: string }
  | { type: "penumbra",  assetId: Hash }
  | { type: "aztec",     assetId: Hash }
  | { type: "native",    id: Hash };

function computeCommitment(note: Note): Commitment {
  const assetCommitment = H_2(DOMAIN_ZKA_ASSET, canonicalAssetIdField(note.asset));
  const inner = H_3(assetCommitment, field(note.value), note.recipient);
  return H_2(inner, note.randomness);
}

// Standalone and other non-Aztec deployments only; §8.1 delegates Aztec spends.
function computeNullifier(note: Note, sk: SpendingKey): Nullifier {
  return H_2(H_2(DOMAIN_ZKA_NULLIFIER, note.commitment), sk);
}

DOMAIN_ZKA_ASSET = deriveFieldTag("zka/asset"). The settlement-specific canonical asset-id projection is defined by the applicable deployment or bridge profile. The inner and outer note-commitment hashes are untagged; their distinct fixed arities and nesting are normative.

BoundedUnsigned is a profile-selected unsigned integer domain. Each deployment profile MUST publish its width and inclusive maximum. Active note values are in 1..=maximum; zero is reserved for an inactive or absent fixed-width slot. Before every Field-to-unsigned-integer conversion, an implementation MUST range-check the source value against its profile domain. This applies to deposits, transfers, withdrawals, popped input-note values, and change outputs. Conservation arithmetic MUST fail closed on aggregate overflow and MUST NOT truncate or wrap.

The Aztec-native ZKA/Pay profile selects u128 with maximum = u128::MAX, so it accepts active values in 1..=u128::MAX. The Base pool's 2^120 - 1 limit and any other narrower limit are profile-specific, not cross-environment cryptographic bounds. An adapter or import/export profile that targets a narrower destination MUST reject a value above the destination maximum before narrowing; it MUST NOT truncate, wrap, saturate, or otherwise silently coerce it.

2.3.1 Note randomness generation

For every active output note, the holder implementation MUST generate randomness internally from an operating-system cryptographically secure random number generator. Sampling MUST cover the complete BN254 scalar field by rejection sampling canonical 32-byte representatives; modulo reduction of a narrow, biased, deterministic, user-supplied, or protocol-derived input is not conforming. Zero is never a valid active-output randomness value. Inactive fixed-width output slots continue to use zero.

A production SDK, wallet, bridge adapter, or rotation path MUST NOT accept caller-supplied note randomness. It MUST reject any reuse it can detect before proof construction, including reuse from an input note and repetition between active outputs in one transaction. A holder implementation MUST retain a bounded non-reversible in-process fingerprint window for values it generates so a recent accidental repeated CSPRNG output fails closed without unbounded memory growth. Deterministic constructors MAY exist only in an explicitly non-production test-vector build and MUST reject zero.

The transfer circuits enforce only witness-visible safety checks: each active output randomness is nonzero and differs from every active input and earlier active output randomness in the same transition. These constraints do not prove entropy, unpredictability, CSPRNG use, or uniqueness against notes absent from the witness; those remain generation-boundary obligations.

Randomness, note openings, and their reuse fingerprints are private material. Errors, logs, events, debug formatting, and telemetry MUST NOT include them. Generation failures use a stable redacted error and fail closed. Preparation and submission APIs that span multiple calls MUST retain the exact opening in holder memory (or holder-controlled encrypted storage); they MUST NOT regenerate an opening after publishing its commitment. A post-send transport, receipt, or event-decoding failure MUST return recoverable holder state containing that exact opening and any known transaction hash rather than a bare error that discards them. Settlement adapters MUST reconcile or resubmit only the exact retained transaction; they MUST NOT rebuild it with fresh note randomness. Confirmation requires a matching canonical settlement event for the retained note commitment (and transaction hash when known).

2.4 Merkle Tree

interface MerkleTree {
  depth: 32;                   // Supports 2^32 notes
  hashFunction: "Poseidon2";   // BN254 field
  root: Hash;
  leaves: Commitment[];
  nextIndex: u32;
}

Merkle path verification uses the untagged binary parent H_2(left, right) over the depth-32 path (zka_lib::compute_merkle_root). Path-index bits are consumed least-significant-bit first across tree levels. The empty leaf is zero and the depth-32 empty root is obtained by 32 applications of H_2(empty, empty). Conforming implementations MUST use this Poseidon2 construction and MUST NOT substitute a Pedersen-based Merkle helper.

The canonical append source for a ZKA deployment MUST be the settlement contract, settlement validity predicate, or deployment-defined append log that authorizes note creation. Balance servers, wallets, mirrors, and indexers MAY maintain local Merkle trees, but they MUST be reconstructable from the canonical append source and MUST NOT be authoritative for leaf order or root validity.

Every deposit, transfer, bridge import, or settlement action that creates a ZKA note MUST append that note commitment exactly once in deterministic settlement order. The leaf index is the zero-based append position in that canonical order. If multiple commitments are created by one settlement action, their intra-action order MUST be specified by the deployment profile and MUST be stable across all clients.

Canonical append events MUST expose enough public or holder-authorized data for clients and indexers to reconstruct the tree:

interface CommitmentAppendEvent {
  commitment: Commitment;
  leafIndex: u32;
  previousRoot: Hash;
  newRoot: Hash;
  context: Hash; // deployment, chain, asset-domain, and circuit-domain binding
}

A verifier, settlement contract, or validity predicate MUST accept Merkle roots only from a deployment-defined acceptedRootSet. The deployment profile MUST define stale-root handling as either:

Roots outside the accepted window MUST be rejected with a machine-readable stale-root error. Rejection of a stale root MUST NOT invalidate the note; the holder can regenerate a Merkle path against an accepted root and resubmit.


3. ZKA/Pay: Private Transfers

3.1 Transfer Circuit (zka_transfer)

Circuit constraints (in Noir):

  1. Input note Merkle membership
  2. Nullifier correctness: derived from commitment + spending key
  3. Ownership: input note recipient matches derived address
  4. Output commitment correctness
  5. Value conservation: input.value == output.value, output.value > 0
  6. Asset conservation: input.asset == output.asset, asset commitment matches
  7. Context binding: proof bound to epoch/chain
interface TransferProof {
  publicInputs: {
    nullifier: Nullifier;
    outputCommitment: Commitment;
    merkleRoot: Hash;
    context: Hash;             // H(epoch, chainId, applicationId)
    assetCommitment: Hash;     // H(asset.assetId)
  };
  privateInputs: {
    inputNote: Note;
    inputMerklePath: MerklePath;
    spendingKey: SpendingKey;
    outputNote: Note;
  };
  proof: UltraHonkProof;
}

3.2 Withdrawal Circuit (zka_withdrawal)

Consumes a note and reveals amount, asset, and external recipient. Used for bridge exits (ZKA shielded pool → external chain).

Per Safeguard 5 (§1.6), the withdrawal circuit MUST NOT take any compliance predicate, credential, or binding proof as an input or constraint. Exit to self-custody is unconditional.

interface WithdrawalProof {
  publicInputs: {
    nullifier: Nullifier;
    merkleRoot: Hash;
    amount: u64;               // Revealed
    asset: AssetDescriptor;    // Revealed
    externalRecipient: Address; // Revealed
    context: Hash;
  };
  proof: UltraHonkProof;
}

3.3 Root Update Circuit (zka_root_update)

Used by deployments that anchor an append-only commitment tree on an external rollup or L1 contract. Proves that a claimed newRoot is the correct Poseidon2 incremental insertion of the public deposit list followed by a prover-supplied private blob-commitment list into a prior accepted prevRoot. The proof preserves the order of the supplied values, but by itself does not prove that the private list came from published blob data and therefore does not prevent omission or substitution of blob commitments.

interface RootUpdateProof {
  publicInputs: {
    prevRoot: Hash;
    newRoot: Hash;
    prevTotal: u64;                       // commitments folded so far
    newTotal: u64;                        // commitments folded after this update
    blobKzgCommitment: Hash;              // legacy-named non-zero proof tag; not a blob binding
    depositCommitments: Hash[];           // ordered by on-chain event-log position
  };
  privateInputs: {
    blobCommitments: Hash[];              // prover-supplied; not bound to blobKzgCommitment
    intermediatePaths: MerklePath[];      // one path per insertion, depth = MERKLE_DEPTH (32)
  };
  proof: UltraHonkProof;
}

Circuit constraints (in Noir):

  1. Count consistency: newTotal - prevTotal == depositCommitments.length + blobCommitments.length
  2. Deterministic interleave order: deposit commitments first (in event-log order), then blob commitments (in blob-payload order)
  3. Incremental insertion: fold each commitment in order into the Merkle tree starting from prevRoot using intermediatePaths; the final fold MUST equal newRoot
  4. Hash function: Poseidon2 over BN254 per §2.1
  5. Tree depth: MERKLE_DEPTH = 32 per §2.4
  6. Legacy blob-tag check: blobKzgCommitment is a public input and MUST be non-zero; this binds only the exact tag value into the proof and establishes no KZG opening, blob preimage, or relation to the private blobCommitments

The blobKzgCommitment public input is not sufficient to bind the private values folded by this circuit. The immutable Base APLBridge passes a caller-supplied value directly into this public-input slot and never executes BLOBHASH; the circuit independently accepts any non-zero value. Consequently, that deployment and circuit establish no blob data-availability or blob-to-leaf binding property. A successor claiming blob data availability MUST compare the same-transaction BLOBHASH result on-chain and verify that the blob's canonical contents encode the private commitments folded by the proof.

A successor profile claiming EIP-4844 data availability MUST (1) obtain the blob versioned hash with BLOBHASH in the same blob-carrying transaction and compare it with a proof-bound public input, and (2) verify a cryptographic relation showing that the ordered commitment projection decoded from the blob's canonical announcement log equals the exact private commitments folded into newRoot. A BLOBHASH comparison alone binds the transaction to a blob, not the circuit's private witness values to that blob.

3.4 Immutable Base WETH9 Pool Profile

Strategic status (informative). This is an interim, non-production EVM deployment profile, not ZKA's strategic flagship or a production-certified target. The Aztec-native, owner-direct ZKA/Pay profile is the strategic flagship, subject to the official V6 resolution and audit-qualified activation gates in ADR-0010. This status statement changes neither this profile's normative circuit, state, or nullifier requirements nor its historical evidence.

ZKA/Pay defines an additive, immutable, single-asset settlement profile for Base WETH9. Its four circuits are:

Circuit Purpose
zka_pay_deposit Binds a queued note commitment to the exact public WETH deposit amount and immutable pool/asset context; the contract stores the exact announcement-hash limbs with the queue entry.
zka_pay_transition Proves a private payment with up to two inputs and three ordered outputs (recipient, change, fee).
zka_pay_batch Recursively verifies payments, consumes the mandatory deposit prefix, advances both roots, and binds canonical batch calldata.
zka_pay_withdrawal Consumes a note, advances the sparse nullifier root, and releases the proof-bound amount to the proof-bound Base recipient.

Each accepted deposit appends the immutable tuple (queue index, note commitment, amount, full announcement Keccak-256 high/low u128 limbs, depositor proof hash). A batch computes deposit_prefix_digest from that stored queue range. It separately computes batch_data_digest from strict- parsed canonical calldata, recomputes every deposit announcement hash, and requires the calldata commitment, amount, and both hash limbs to match the immutable queue entry before verification or state advancement. The batch circuit constrains the same five deposit fields into both digest streams.

The pool context is contract-derived, never a deployer-selected constructor value. It uses the exact UTF-8 tag zka/pay/pool-context/v1 and the field-count-prefixed BN254 Poseidon2 fold over profile version, Base chain id, pool and asset addresses, asset commitment, full Keccak-256 limbs of all four circuit ids, and every frozen pool bound/depth. The complete ordered 22-field stream is normative in the pool SDD. Changing any field requires a new pool.

The complete identifiers, flattened public-input order, fixed bounds, tree depths, canonical batchData encoding, digest folds, and deterministic error taxonomy are normative in the ZKA/Pay pool SDD. The machine-readable public-input encoders are schemas/public-inputs/zka_pay_*.json.

This profile supersedes neither the existing bridge circuits (§7) nor the existing ZKA note (§2.3). It uses the existing note-commitment semantics in a separate pool state domain with separate commitment/nullifier trees, escrow, verifiers, and accepted-root history.


4. ZKA/Coord v2: Profile-Bound Recursive Execution

ZKA/Coord v2 uses a fixed, 30-field execution receipt and bounded recursive DAG composition. The normative ABI, domains, profile-manifest contract, workflow-definition encoding, base and recursive cases, trust boundaries, failure codes, migration rules, and graduation gate are defined in docs/sdd/sdd_coord_v2.md. The machine-readable ABI is schemas/public-inputs/zka_coord_v2.json.

A final proof attests that every accepted step was proven by an execution profile in a verifier-selected policy, every active predecessor proof was cryptographically verified, and ordered predecessor outputs exactly equal the successor inputs. Workflow definition, instance, context, initial state, topology, profile artifacts, verification keys, assurance class, and inactive padding are all bound.

The three assurance classes are intentionally distinct: ZK-native execution, proof-producing external execution, and attested execution. The latter proves correct checking of a named trust anchor, not physical-world truth. ZKA defines no global profile allowlist.

ZKA/Coord v2 is the canonical pre-launch draft. It remains unaudited and not production-supported until the signed independent-review gate in docs/security/coord_v2_audit_gate.md passes. The four-field base circuit, three-field aggregate, and five-field bundle object are invalid and absent from active schemas, artifacts, registries, and SDK decoders. A conforming verifier MUST reject them; there is no legacy alias, width-based interpretation, or fallback verification key.

The public Rust API is canonical and unversioned (CoordWorkflowBuilder, CoordProver, CoordVerifier). Version markers remain mandatory in the cryptographic identities that prevent cross-protocol substitution, including the receipt/schema identifiers, circuit and artifact IDs, policy documents, manifest formats, and hash domains.

interface CoordFinalPublicInputsV2 {
  receipt: CoordReceiptV2; // exact 30-field order from the normative SDD/schema
  graphCommitment: Field;
  proofNodeCount: u32;     // 1..=4096
  maxDepth: u32;           // 1..=32
}

interface CoordVerifierPolicyV2 {
  policyDocument: CoordVerifierPolicyDocumentV2;
  verifierPolicyRoot: Field;         // MUST equal recomputed policyDocument root
  acceptedAssuranceClasses: Array<1 | 2 | 3>;
  workflowDefinition: CoordWorkflowDefinitionV2;
  expectedInitialStateCommitment: Field; // MUST equal workflowDefinition.initialStateCommitment
  expectedWorkflowInstanceId?: Field;
  expectedContext?: Field;
  acceptedProfileManifestHashes: Field[];
}

Serializers flatten receipt first and then the three aggregate fields, for a fixed total of 33 public field elements.

The reference verifier rejects raw recursive proofs above 65,536 bytes before backend dispatch and canonical JSON proof envelopes above 524,288 bytes before parsing. The typed CoordProof decoder also stops at byte 65,537 while streaming the proof array, so a compliance-bundle decoder cannot first allocate an attacker-selected oversized proof vector. Other implementations MUST publish and enforce equivalent conservative byte ceilings at every decoding and verification entry point. Proving time and memory still depend on active fan-in and recursion depth; implementations MUST publish measured bounds and MUST NOT claim workflow-length-independent proving cost.

An Aztec deployment is conforming only through an explicit, manifest-bound profile whose claim is tested independently. Kernel composition alone does not silently inherit the Coord v2 claim. The reference SDK publishes the off-chain UltraHonk path. An Aztec target additionally requires an independently registered profile and target verifier; no built-in Aztec Coord contract is provided. An EVM request fails with UnsupportedTarget until an exact EVM artifact and verifier are published, and MUST NOT fall back to an off-chain VK.


5. ZKA/Attest: ZKC Credential Integration (Normative)

This section defines ZKC credential integration in the ZKA protocol layer using Noir, UltraHonk, BN254, and Poseidon2. The constraint logic is expressed independently of deployment-specific performance parameters.

5.1 Identity Binding Model

The credential binding key links ZKA and ZKC identities without revealing either. ZKC v0.4 defines three presentation profiles. Legacy linkable v1 exposes the stable holderCommitment; v2 scoped exposes only H("zkc/presentation-scope:v1", cbk, scopeId); v2 anonymous exposes neither handle (both presentation public inputs are zero). A scoped pseudonym is unlinkable across distinct scopes unless the principal discloses the link.

IDENTITY BINDING
================

ZKA Identity                    ZKC Identity
------------                    ------------

   address                      holderCommitment
      |                              |
      v                              v
  H("zka/address", vk)          H("zkc/binding", cbk)
      |                              |
      +--------------+---------------+
                     |
                     v
               masterSeed  (private witness only)

5.2 Identity Binding Circuits (zka_credential, zka_transfer_binding_v2)

Proves that a ZKC credential belongs to the same identity as a ZKA note, without revealing the master seed.

IDENTITY BINDING CIRCUIT
========================

Public Inputs:
  - noteCommitment: Commitment
  - holderCommitment: Hash
  - merkleRoot: Hash
  - context: Hash

Private Inputs:
  - masterSeed: Uint8Array[32]
  - note: Note
  - merklePath: MerklePath

Constraints:

1. KEY DERIVATION  (per AFP-KDC v1.0.2 §A.6 — AFP Appendix A)
   sk  = H("zka/spending", masterSeed)
   vk  = H("zka/viewing", sk)
   addr = H("zka/address", vk)
   cbk  = H("zkc/credential", masterSeed)
   // masterSeed is the per-entity sub-seed M' under AFP entity identity

2. NOTE OWNERSHIP
   note.recipient == addr
   note.commitment == noteCommitment
   verifyMembership(note.commitment, merklePath, merkleRoot)

3. CREDENTIAL BINDING
   H("zkc/binding", cbk) == holderCommitment

4. CONTEXT BINDING
   // context bound into the proof; prevents replay to a different
   // session/counterparty (context includes a session id)
interface IdentityBindingProof {
  publicInputs: {
    noteCommitment: Commitment;
    holderCommitment: Hash;
    merkleRoot: Hash;
    context: Hash;
  };
  privateInputs: {
    masterSeed: Uint8Array;     // 32 bytes — proves ownership of both
    note: Note;
    merklePath: MerklePath;
  };
  proof: UltraHonkProof;
}
Parameter UltraHonk / BN254
Proving system UltraHonk (Barretenberg)
Circuit size ~2^15 UltraHonk gates
Proof size ~2 KB
Verification <50 ms (native), <100 ms (browser)
Solidity gas <300k gas

The four-public-input zka_credential circuit above and the five-public-input zka_transfer_binding circuit in §5.3 are the frozen linkable v1 profile. They remain accepted for backward compatibility.

For ZKC v0.4 v2 payments, zka_transfer_binding_v2 is normative. It privately derives sk = H("zka/spending", masterSeed) and cbk = H("zkc/credential", masterSeed), proves that sk owns the input note, and constrains presentationPseudonym = H("zkc/presentation-scope:v1", cbk, scopeId). Its public inputs are (nullifier, outputCommitment, merkleRoot, context, presentationPseudonym, scopeId). scopeId and presentationPseudonym MUST both be non-zero. Neither masterSeed, cbk, nor holderCommitment is public. An anonymous v2 presentation cannot bind a payment note to a credential subject and MUST be rejected for a payment bundle; it remains valid for a payment-free bundle when the bundle members retain the authenticated AFP session-context field projection and the ZKC proof separately matches AFP's authenticated presentation-purpose context.

Issuer-signature verification is NOT duplicated here: a ZKA binding proof attests only seed-linkage and note ownership; valid issuance remains in ZKC's per-credential circuits carried by compliance.proofs[]. context MUST bind the relevant replay-protection context. When carried by afp:envelope:v1, it equals session_context_field, the canonical BN254 projection of AFP's authenticated raw session_context defined in §5.4.

5.3 Combined Transfer + Binding Circuit

For efficiency, a combined circuit proves transfer validity and compliance binding in a single proof. zka_transfer_binding is the frozen linkable-v1 circuit:

COMBINED TRANSFER + BINDING CIRCUIT
===================================

Public Inputs:
  - nullifier: Nullifier
  - outputCommitment: Commitment
  - merkleRoot: Hash
  - context: Hash
  - holderCommitment: Hash      // for compliance binding

Private Inputs:
  - masterSeed: Uint8Array[32]
  - inputNote: Note
  - inputMerklePath: MerklePath
  - outputNote: Note

Constraints:
  // all transfer constraints from §3.1
  +
  // binding constraint
  H("zkc/binding", H("zkc/credential", masterSeed)) == holderCommitment

Parameters (UltraHonk / BN254):
  - ~2^16 gates, ~2 KB proof, single proof for transfer + binding

zka_transfer_binding_v2 uses the ordered public inputs presentationPseudonym, scopeId rather than holderCommitment, and uses the private masterSeed witness rather than spendingKey. The transfer spending key and ZKC v0.4 credential binding key are derived independently from that same secret in-circuit. This is the required v2 payment proof and prevents a prover from pairing a note owned by one subject with a scoped credential presentation from another.

5.4 Compliance Bundle

A Compliance Bundle is the unit a verifier checks: it may carry a ZKA transaction proof, zero or more atomic ZKC predicate proofs, the identity binding that links a payment note to a credential, and — optionally — a recursive ZKA/Coord work-attestation proof (§4). The bundle is a ZKA-side envelope that wraps, but does not redefine, ZKC compliance proofs: each element of compliance.proofs[] is an authoritative ZKC zkc:proof:v1 (linkable) or zkc:proof:v2 (anonymous/scoped) object from ZKC v0.4. The bundle envelope remains independently versioned as zka:bundle:v1; carrying the canonical pre-launch Coord v2 draft proof does not change that envelope version. The carried Coord, ZKC, and presentation-profile identities independently select their exact verifier/VK. Conforming cross-protocol implementations MUST select exactly one combination from the canonical zka:protocol-family:2026-09-10.1 manifest and compatibility matrix; no match or multiple matches is invalid.

Bundle profiles and the substantiveness rule. Both transaction and coord_proof are OPTIONAL members, but a bundle MUST carry at least one of {transaction, coord_proof} — a bundle with neither attests no substantive ZKA-side work and is invalid (EMPTY_BUNDLE). compliance.proofs[] always accompanies a substantive proof (carried when the counterparty requires predicates); it is never the sole content of a valid bundle. The admissible profiles are:

The normative reciprocal compute/acknowledgement encodings and assurance boundaries are specified in sdd/sdd_coord_reciprocal_data_exchange.md. vxp.reciprocal MUST travel through the existing zka:bundle:v1 pure work-attestation shape inside an authenticated AFP session, with the bundle context equal to AFP's derived session_context_field; it introduces no new envelope or session-binding primitive.

transaction is OPTIONAL because non-monetary flows such as PRP reconciliation and vxp.reciprocal carry compliance/work evidence over zka:bundle:v1 with no payment leg. Requiring a payment proof would make those flows unable to form a valid bundle. The substantiveness rule admits pure work-attestation while leaving every payment bundle valid.

The bundle is also the single ZKA-owned cryptographic payload that AFP's session envelope (afp:envelope:v1, AFP v0.3.12-draft §12.7) carries verbatim across a tenant boundary. The OPTIONAL coord_proof and OPTIONAL transaction members do not change the zka:bundle:v1 envelope identity: payment/compliance bundles remain valid instances, and transaction-absent bundles use the same explicitly optional field encoding.

Authenticated context split is mandatory. Verification MUST receive both context classes from the authenticated AFP outer interaction; it MUST NOT copy either expectation from the untrusted bundle or proof. AFP retains its raw authenticated session_context as exact X_ plus 64 lowercase hexadecimal digits. Let d be those 32 payload bytes and let q = 21888242871839275222246405745257275088548364400416034343698204186575808495617. The fixed projection is s = OS2IP_BE(d) mod q; s = 0 is invalid, without retry or substitution, and otherwise session_context_field is s serialized as exact 0x plus 64 lowercase hexadecimal digits. The raw X_... value remains the AFP session identifier and is authenticated before projection; modulo-field equality is never session authentication. session_context_field binds the bundle members, while the selected afp:zkc-presentation-context:v2 mode authenticates the separate presentation_proof_context used by ZKC. A scoped v2 payment additionally requires a verifier-selected scopeId. A malformed or zero projection, a session-member mismatch, or a ZKC proof/presentation-context mismatch is invalid.

session_context = require_exact_afp_X_digest(afp.authenticated.session_context)
session_context_field = nonzero_bn254_projection(session_context)
presentation_proof_context = afp.authenticated.context(
    selector = "afp:zkc-presentation-context:v2",
    trust_flow = verifier_selected_authenticated_flow,
    purpose = verifier_selected_authenticated_purpose,
)

require(bundle.metadata.context == session_context_field)
if bundle.transaction is present:
    require(bundle.transaction.publicInputs.context == session_context_field)
if bundle.coord_proof is present:
    require(bundle.coord_proof.receipt.context == session_context_field)
for envelope in bundle.compliance.proofs:
    require(envelope.metadata.context.value == presentation_proof_context)
    if envelope.version == "zkc:proof:v2":
        for predicate in envelope.proofs:
            require(predicate.publicInputs.proof_context == presentation_proof_context)

For afp:zkc-presentation-context:v2, the authenticated AFP record contains exactly kind, purpose, credential_subject_aid, session_context, apr_verifier_aid, presentation_profile, and scope_policy; its session_context member is the original authenticated X_... value, not session_context_field. Its trust-flow selector is one of afp:trust-flow:telemetry, afp:trust-flow:bilateral, afp:trust-flow:introduction, or afp:trust-flow:open-federation, and purpose MUST equal that selector. For the scoped profile, scope_policy MUST be protocol:afp: followed by a nonempty AFP-owned profile-policy hash; it is null for anonymous or linkable profiles. presentation_proof_context is int_be(SHA-256(RFC8785_JCS_UTF8(record))) mod q, serialized as exactly 32 big-endian bytes of lowercase 0x hexadecimal. A zero reduction is invalid. The selector is part of the matched protocol-family tuple. A verifier MUST recompute this field before proof verification and MUST NOT substitute either session-context representation for presentation_proof_context. This selector and comparison change no zka:bundle:v1 wire shape or ZKC circuit public-input ABI.

AFP verifier-result boundary. The AFP adapter result has exactly the three top-level members {proof_context, credential_subject_aid, proven_predicates}. proven_predicates is a nonempty ordered array of exact six-member records: {predicate_id, predicate_version, root_class, root_authority_identifier, attestation_window, subject_aid}. Every member is present; unavailable values are explicit null. The identifier is versioned, predicate_version is the matching nonempty decimal suffix, and duplicate identifiers are invalid. The adapter verifies each complete normalized record against its verified evidence snapshot, rather than accepting an identifier-only claim. At the presentation-policy seam, only predicate_id, predicate_version, root_class, and root_authority_identifier have authenticated policy comparands; attestation_window and subject_aid remain free there and receive their specified Introduction bindings later. A non-null attestation_window is {starts_at, ends_at} in canonical whole-second UTC with starts_at < ends_at.

The verifier's sibling required_predicate_policy comparand is not part of the result. It contains unique sorted accepted_predicate_ids, required_predicate_ids, and accepted {root_class, root_authority_identifier} pairs. Accepted predicates are nonempty, required predicates are a subset, and the telemetry instance-attestation predicate is accepted only when at least one root pair is pinned. AFP telemetry has a broader closed catalog; this conformance profile requires afp.telemetry.instance_attestation.v2. That record requires a nonempty subject AID, a valid window, and an accepted root-authority pair whose class is exactly one of root:build-provenance, root:tee-attestation, or root:publisher-manifest. For other predicates, root class and root authority are null; bilateral, introduction, and open-federation otherwise retain caller-policy-selected predicate pluralism. Structural validation never substitutes for AFP's KEL/delegation/revocation resolution or its later request-window/presenter checks.

The CI-gated AFP presentation-seam classification is:

Proven-predicate field Class at presentation seam Independent comparand / later binding
predicate_id bound required/accepted predicate policy and exact verified ZKC statement
predicate_version bound version suffix of predicate_id and exact verified statement
root_class bound null for ordinary predicates; supported class and accepted class/authority pair for telemetry v2
root_authority_identifier bound null for ordinary predicates; accepted class/authority pair plus AFP authority resolution for telemetry v2
attestation_window free shape and presence are validated here; Introduction later binds it to its authenticated request window
subject_aid free shape and presence are validated here; Introduction later binds it to the authenticated presenter/entity-or-instance rule

“Free” means the presentation seam has no independent equality comparand for that value; it does not mean an application may skip the specified shape, presence, or later Introduction checks.

Separate non-result adapter-assurance metadata MUST identify an issuer-authenticated AID claim or independently verified companion subject-binding proof as the basis for credential_subject_aid; it MUST NOT be copied from ZKC evidence. A generic ZKC KYC or jurisdiction predicate alone cannot establish that subject and MUST be rejected. The optional predicate subject_aid is not universally equal to this result subject because an instance predicate may identify an entity or one of its instances. When a bundle binds a payment subject, it MUST equal the verified result credential_subject_aid. This is an AFP adapter contract only: ZKC v0.4.1 circuits, verification keys, proof envelopes, and public-input ABI remain unchanged.

Verifier selection is part of verification. For each ZKC predicate, the parser resolves the exact (envelopeVersion, presentationProfile, proofType) tuple and supplies it to the ZKC verifier/VK registry; an unsupported or mismatched tuple fails closed. Structural parsing validates a versioned proofType identifier but MUST NOT infer its version from the envelope suffix: registry policy owns valid mixed-version tuples, including selective-disclosure v1 in a v2 presentation envelope. For the payment binding, the verifier receives the exact selected circuit plus the complete ordered public statement: five fields for zka_transfer_binding v1 or six fields for zka_transfer_binding_v2. A verifier that has no configured cryptographic implementation MUST reject; structural proof-byte checks are not proof verification.

ZKM carriage boundary. zka:bundle:v1 currently defines no authorization or ZKM member. An implementation MUST NOT place a ZKM proof in compliance.proofs[], reinterpret coord_proof as mandate authorization, or claim recursive ZKM carriage through this envelope. A future ZKA/ZKM integration must select either an additive bundle member/profile or a direct settlement interface and must make the ZKA note/nullifier effects and ZKM mandate transition atomic. Until then, ZKM's separately versioned, spend-conjoined statement remains outside zka:bundle:v1 and is accepted only through a ZKM-aware settlement integration and ZKM's deployment registry.

interface ComplianceBundle {
  transaction?: {                      // OPTIONAL — present for PAYMENT
    type: "transfer" | "deposit" | "withdrawal";   //   flows; OMITTED for pure work-attestation
    proof: UltraHonkProof;             //   bundles (PRP reconciliation, non-monetary VXP).
    publicInputs: ZKAPublicInputs;
  };
  compliance: {
    proofs: (ZKCProofV1 | ZKCProofV2)[]; // Version/profile-discriminated ZKC v0.4 objects;
                                       // each proves ONE atomic predicate (Safeguard 2).
                                       // MAY be empty when the counterparty requires none;
                                       // compliance always ACCOMPANIES a substantive proof,
                                       // it is never the sole content of a valid bundle.
    aggregation?: UltraHonkProof;      // optional recursive aggregate (ZKC v0.4 §5.2)
  };
  binding?: ({                         // Legacy linkable v1 (version may be omitted).
    proof: IdentityBindingProof;
    transactionCommitment: Hash;
    zkcEnvelopeVersion?: "zkc:proof:v1";
    holderCommitment: Hash;
  } | {                                // ZKC v0.4 scoped v2; no stable holder handle.
    proof: TransferBindingV2Proof;
    transactionCommitment: Hash;
    zkcEnvelopeVersion: "zkc:proof:v2";
    presentationPseudonym: Hash;
    scopeId: Hash;
  });
  coord_proof?: {                      // OPTIONAL — ZKA/Coord recursive work-attestation
    proof: UltraHonkProof;             //   profile-bound recursive DAG proof (§4)
    receipt: CoordReceiptV2;           // 30-field final receipt plus graph
                                       // commitment/count/depth;
                                       // receipt.context MUST equal metadata.context
    artifact: CoordArtifactRef;        // manifest-selected circuit/schema/VK identity
  };
  metadata: {
    version: "zka:bundle:v1";
    generatedAt: Timestamp;
    expiresAt: Timestamp;
    context: Hash;                     // Canonical nonzero BN254 replay context.
                                       // Under AFP, equals session_context_field.
  };
}
async function verifyComplianceBundle(
  bundle: ComplianceBundle,
  requirements: ComplianceRequirements,
  trustPolicy: TrustPolicy,
  authenticated: {
    bundleContext: Hash;               // Under AFP, recomputed session_context_field.
    presentationContext: {
      profile: "afp:zkc-presentation-context:v2";
      value: Hash;
    };
    selectedScopeId?: Hash;
  }
): Promise<VerificationResult> {
  const errors: VerificationError[] = [];

  // Expectations are verifier-supplied, never learned from `bundle`.
  if (bundle.metadata.context !== authenticated.bundleContext)
    errors.push({ code: "BUNDLE_INCONSISTENT", details: "unauthenticated bundle context" });
  if (bundle.transaction !== undefined &&
      bundle.transaction.publicInputs.context !== authenticated.bundleContext)
    errors.push({ code: "BUNDLE_INCONSISTENT", details: "transaction context mismatch" });

  // 0. Substantiveness — a valid bundle MUST carry at least one
  //    substantive proof. `compliance.proofs[]` alone is never sufficient.
  if (bundle.transaction === undefined && bundle.coord_proof === undefined)
    errors.push({ code: "EMPTY_BUNDLE",
                  details: "bundle carries neither transaction nor coord_proof" });

  // 1. ZKA transaction proof (OPTIONAL) — verify only when present.
  //    A pure work-attestation bundle (profile b) has no transaction leg.
  if (bundle.transaction !== undefined) {
    if (!(await verifyUltraHonk(bundle.transaction)).valid)
      errors.push({ code: "INVALID_ZKA_PROOF" });
  }

  // 2. ZKC predicate proofs — each predicate MUST be open-source and
  //    inspectable per Safeguard 4; trustPolicy enumerates accepted
  //    predicate providers (no canonical provider — Safeguard 1).
  //    Verified over whatever proofs ARE present (the set MAY be empty).
  if (!(await verifyZKCProofs(bundle.compliance, requirements, trustPolicy, authenticated)).valid)
    errors.push({ code: "INVALID_ZKC_PROOF" });

  // 3. Binding links the payment NOTE <-> ZKC credential. It is present IFF
  //    `transaction` is present (no payment ⇒ no note ⇒ nothing to bind).
  if (bundle.transaction !== undefined) {
    if (bundle.binding === undefined) {
      errors.push({ code: "BUNDLE_INCONSISTENT",
                    details: "transaction present but binding absent" });
    } else {
      const statement = resolveBindingStatement(bundle.transaction, bundle.binding);
      if (!(await verifyManifestSelectedBindingProof(statement)).valid)
        errors.push({ code: "INVALID_BINDING" });

      // 4. Bundle consistency — note<->credential checks, IFF the relevant
      //    members are present.
      if (bundle.binding.transactionCommitment !==
          bundle.transaction.publicInputs.outputCommitment)
        errors.push({ code: "BUNDLE_INCONSISTENT", details: "tx commitment mismatch" });
      for (const envelope of bundle.compliance.proofs) {
        const target = resolveZkcVerifierTarget(
          envelope.version, envelope.presentationBinding?.profile, envelope.proofs);
        if (!target.valid)
          errors.push({ code: "INVALID_ZKC_PROOF", details: "unsupported version/profile" });
        else if (!bindingSubjectMatchesEnvelope(bundle.binding, envelope))
          errors.push({ code: "BUNDLE_INCONSISTENT", details: "binding subject/profile mismatch" });
      }
    }
  } else if (bundle.binding !== undefined) {
    // A transaction-absent bundle MUST NOT carry a note<->credential binding:
    // there is no note for it to bind (it would be unanchored/meaningless).
    errors.push({ code: "BUNDLE_INCONSISTENT",
                  details: "binding present without a transaction" });
  }

  // When `transaction` is absent, no note<->credential binding exists. The
  // bundle and any Coord proof remain anchored to authenticated.bundleContext.
  // Each ZKC proof is independently checked against
  // authenticated.presentationContext, whose value AFP derives from the
  // authenticated flow, purpose, session, subject, verifier, profile, and scope
  // policy. Neither context class may be substituted for the other.

  // 5. Freshness — bundle expiry AND ZKC accumulator-epoch recency.
  //    Per ZKC v0.4 §6.2, each ZKC proof's accumulator epoch MUST be
  //    within MAX_EPOCH_LAG = 10 epochs of the verifier's current epoch.
  if (bundle.metadata.expiresAt < Date.now())
    errors.push({ code: "BUNDLE_EXPIRED" });
  if (!zkcEpochsWithinLag(bundle.compliance.proofs, /*MAX_EPOCH_LAG*/ 10))
    errors.push({ code: "ZKC_EPOCH_STALE" });

  // 6. ZKA/Coord work-attestation (OPTIONAL) — verify only when present.
  //    A bundle without coord_proof is unaffected by this step.
  if (bundle.coord_proof !== undefined) {
    // 6a. Verify the manifest-bound Coord v2 recursive artifact. This authenticates
    //     profile-adapter and predecessor proofs plus their constrained receipts;
    //     step 6c still applies the verifier's own policy and completion definition.
    //     An Aztec proof is accepted only through an independently conforming
    //     manifest-bound profile (§8.2), never by kernel composition alone.
    if (!(await verifyManifestBoundUltraHonk({
            proof: bundle.coord_proof.proof,
            publicInputs: bundle.coord_proof.receipt,
            circuit: "zka_coordination_v2_recursive",
            artifactPolicy: trustPolicy.coord })).valid)
      errors.push({ code: "INVALID_COORD_PROOF" });

    // 6b. Context binding — the coordination proof MUST share the bundle context
    //     (and hence, under AFP, session_context_field for the carrying envelope, §12.7).
    if (bundle.coord_proof.receipt.context !== bundle.metadata.context)
      errors.push({ code: "BUNDLE_INCONSISTENT", details: "coord context mismatch" });

    // 6c. Verifier-selected policy and complete-workflow checks. A prover's
    //     public policy root or workflow root is never authoritative.
    const coord = trustPolicy.coord;
    const receipt = bundle.coord_proof.receipt;
    if (coord === undefined) {
      errors.push({ code: "COORD_V2_PROFILE_POLICY", details: "no verifier Coord policy" });
    } else {
      const definition = validateAndHashCoordWorkflow(coord.workflowDefinition);
      const policy = validateAndHashCoordPolicy(
        coord.policyDocument,
        coord.acceptedAssuranceClasses,
        coord.acceptedProfileManifestHashes);
      if (!definition.valid ||
          receipt.workflowDefinitionHash !== definition.root ||
          receipt.stepId !== definition.completionStepId)
        errors.push({ code: "COORD_V2_INCOMPLETE_WORKFLOW" });
      if (!policy.valid || policy.root !== coord.verifierPolicyRoot ||
          receipt.verifierPolicyRoot !== policy.root)
        errors.push({ code: "COORD_V2_PROFILE_POLICY" });
      if (definition.valid && !definition.steps.every(step =>
            coord.acceptedAssuranceClasses.includes(step.assuranceClass)))
        errors.push({ code: "COORD_V2_ASSURANCE" });
      if (definition.valid && !definition.steps.every(step =>
            coord.acceptedProfileManifestHashes.includes(step.profileManifestHash)))
        errors.push({ code: "COORD_V2_ARTIFACT" });
      if (coord.expectedWorkflowInstanceId !== undefined &&
          receipt.workflowInstanceId !== coord.expectedWorkflowInstanceId)
        errors.push({ code: "COORD_V2_SCOPE", details: "workflow instance mismatch" });
      if (receipt.initialStateCommitment !== coord.expectedInitialStateCommitment ||
          (definition.valid &&
           definition.initialStateCommitment !== coord.expectedInitialStateCommitment))
        errors.push({ code: "COORD_V2_SCOPE", details: "initial state mismatch" });
      if (coord.expectedContext !== undefined && receipt.context !== coord.expectedContext)
        errors.push({ code: "COORD_V2_SCOPE", details: "verifier context mismatch" });
    }
  }

  return { valid: errors.length === 0, errors };
}

Conditional verification. Each member is verified IFF present, and the transaction-dependent consistency checks fire only when a payment leg exists:

Identity anchor when transaction is absent. A pure work-attestation bundle still binds to a single party identity, but not through the note↔︎credential binding (which is N/A without a note). The bundle and Coord receipt retain the authenticated AFP session_context_field anchor derived from the raw session identifier. Any ZKC predicate proof has its own authenticated presentation-purpose anchor selected by afp:zkc-presentation-context:v2; its proof context is not a session member and is compared only with AFP's presentation_proof_context. Both comparisons are mandatory, so a cross-flow or cross-purpose presentation cannot be attached to an otherwise valid transaction-absent bundle.

A bundle verification failure has exactly one permitted consequence: the counterparty declines the interaction. It MUST NOT trigger any note invalidation, freeze, or force-transfer (Safeguard 3). This holds for a coord_proof failure as well: a failed (or context-mismatched) coordination proof means the work-attestation is not accepted, never that any note is affected.

Canonical Coord carriage (AFP v0.3.12-draft §12.7, ADR-0003). The optional coord_proof member makes zka:bundle:v1 the single ZKA-owned wire format for ZKA/Coord work-attestation proofs alongside Pay/compliance/binding proofs. A profile MUST NOT author another bundle wire format; it places a ZKA bundle inside afp:envelope:v1. The receipt context equals the bundle metadata context, which AFP derives as session_context_field from its authenticated raw session_context. The pre-launch five-field coordination carriage is invalid.

AFP remains responsible for session framing, routing, authentication, and session_context binding; ZKA remains responsible for the bundle and carried proof semantics.

5.4.1 ZKM Authorization Carriage and Atomic Settlement

ZKM spend mandates are an optional interaction authorization, not a ZKA note-validity rule. A conforming ZKA deployment MAY expose the interface in this section; an ordinary transfer, deposit, withdrawal, force exit, or bundle that does not opt into ZKM MUST remain valid under exactly the pre-ZKM rules.

Canonical carriage decision — direct settlement. ZKA selects the ZKM reference path: the settlement target verifies one zkm:spend-conjoined:v2 UltraHonk proof and applies its ZKA and ZKM effects in one target transaction. The proof already contains the ZKM SPEND transition, the ZKA value-note spend, and the policy predicate over shared wires; a second ZKA transaction proof MUST NOT be accepted as a substitute for that structural conjunction. This revision does not add a zkm_authorization (or any other ZKM member) to zka:bundle:v1. Merely placing unrelated ZKA and ZKM proofs in an envelope is non-conforming.

The spend-conjoined public vector is exactly 14 BN254 field elements, in this order (integers are zero-extended to one field element):

(kind, mandateNullifier, mandateSuccessorCommitment,
 mandateSecondSuccessorCommitment, mandateRoot, revocationRoot,
 revocationScheme, currentTime, settlementContext, assetId,
 zkaNoteRoot, zkaNullifier, zkaRecipientCommitment, zkaChangeCommitment)

kind MUST equal ZKM SPEND (0), revocationScheme MUST equal 2, and mandateSecondSuccessorCommitment MUST be the zero sentinel. The verifier MUST flatten named inputs in this order; it MUST NOT sort fields or accept an alternate encoder. ZKM keeps the private amount off this vector. The two ZKA output commitments bind it inside the proof.

revocationScheme names the ZKM revocation-accumulator profile the proof's non-membership witness was constructed against. Scheme 2 is the depth-254 full-identifier exclusion tree; scheme values 0, 1, and 3..255 are invalid and MUST be rejected before verification. The target treats it as a statement discriminator it checks, never as a policy dial it selects: the registry-pinned VK already accepts exactly one scheme, and the explicit equality check makes a scheme mismatch a deterministic rejection rather than a verification failure whose cause is opaque.

Verifier, registry, and binding checks. Before changing state, a conforming target MUST perform all of the following:

  1. Select the one deployment-registry entry for zkm:spend-conjoined:v2, including its exact VK hash, verifier artifact, public-input encoder, ZKM version, settlement target, and asset profile. The value-moving entry MUST be immutable or version-pinned for the lifetime of the adapter. A proof verified by zkm:mandate-only:v2, a possession, disclosure, revocation, control-attestation, or unknown VK MUST NOT reach a value-moving entry point.
  2. Require settlementContext to equal the target's own non-zero ZKA deployment context. The fixed spend-conjoined circuit also proves that the consumed mandate commits to this domain. A caller-supplied context is never authoritative.
  3. Require assetId to equal the target registry's asset identifier. The fixed circuit proves that the consumed ZKA note and both output notes use the corresponding ZKA asset commitment. A settlement adapter MUST document the exact assetId↔︎ZKA asset-commitment mapping it accepts.
  4. Recognize mandateRoot in its own recent mandate-root history, zkaNoteRoot in its own recent ZKA note-root history, and revocationRoot in its own ZKM revocation-root history. It derives the revocation epoch from that history and applies ZKM freshness policy. Post-cutover, that history MUST contain only scheme-2 roots published at or after the ZKM activation epoch; a scheme-1 root MUST NOT be recognized, relabelled, or reinterpreted under scheme 2. currentTime MUST equal the target clock value substituted for this settlement attempt. revocationScheme MUST equal 2.
  5. Verify the proof against the registry-selected spend-conjoined VK and the exact vector above. That circuit is the same-note/same-holder proof: its public zkaNullifier consumes the ZKA note proven under zkaNoteRoot, while one private holder secret derives the mandate holder key, the consumed ZKA note address, and the ZKA nullifier key. Host code MUST NOT emulate this private equality with caller-supplied holder fields.
  6. Reject if mandateNullifier is already present in the target's single mandate-nullifier set for settlementContext, shared by every accepted ZKM VK, or if zkaNullifier is already present in ZKA's nullifier state. Per-VK mandate-nullifier sets are forbidden: the same mandate note could otherwise be replayed once per statement/VK.

All field elements MUST be canonical BN254 representatives. Required roots, nullifiers, context, asset, and live output commitments MUST be non-zero. Apart from the SPEND kind discriminator, the only zero in a conforming SPEND vector is the unused second mandate-successor slot.

ZKM statement version boundary (fail-closed). The statements selected here are the post-cutover ZKM ones. zkm:spend-conjoined:v1 and zkm:mandate-only:v1 — the 13- and 8-element ABIs of the previous revision, whose witnesses were built against the depth-128 low-half revocation tree and which carry no revocationScheme — are valid only under pre-cutover ZKM keys and roots. A post-cutover deployment registry MUST omit both and MUST NOT reinterpret, upgrade, or cross-verify a v1 proof. It MUST NOT accept a v1 vector under a v2 VK or a v2 vector under a v1 VK. The two profiles never coexist in one registry: ZKM's cutover is atomic, so before activation only the v1 profile is accepted and at or after activation only scheme 2 and its v2 keys are. A deployment that must remain on pre-cutover ZKM is conforming to the previous revision of this section and MUST document that it is pre-cutover; it is not conforming to this one, and a target MUST NOT present the two as interchangeable.

Atomic state transition. After every precondition and proof check succeeds, the target applies exactly these five effects as one transaction:

  1. check-and-insert mandateNullifier into the shared ZKM nullifier set;
  2. check-and-insert zkaNullifier into ZKA nullifier state;
  3. insert mandateSuccessorCommitment into the mandate tree;
  4. insert zkaRecipientCommitment into the ZKA note tree; and
  5. insert zkaChangeCommitment into the ZKA note tree.

Either all five effects and their events become visible, or none do. A target MUST use a transactional state primitive (for example an EVM transaction, a database transaction, or copy-on-write staging) and MUST propagate any late tree, capacity, or callback failure so the two nullifiers roll back with every commitment insertion. It MUST NOT acknowledge, queue, or emit a successful effect before the whole transition commits. All five effects use the target's ordinary ZKA finality semantics.

The non-value entry point verifies zkm:mandate-only:v2 over its distinct 9-element ABI and MAY update only mandate nullifier/commitment state:

(kind, mandateNullifier, mandateSuccessorCommitment,
 mandateSecondSuccessorCommitment, mandateRoot, revocationRoot,
 revocationScheme, currentTime, settlementContext)

kind MUST NOT be SPEND (0) and revocationScheme MUST equal 2. Its ABI contains no ZKA note root, ZKA nullifier, ZKA output commitment, or asset ID; this absence is a deployment-safety property, not a host-side policy flag. The two vectors are the same nine leading elements plus the spend-conjoined form's five-element ZKA value leg, so an implementation MUST distinguish them by registry-selected VK and vector length, never by content sniffing. Both entry points MUST share the mandate-nullifier set.

Exit and revocation boundary. The ordinary ZKA withdrawal and force-exit entry points MUST NOT call the ZKM adapter, inspect mandate state, require a ZKM proof, or consult ZKM revocation/expiry. Revoking or exhausting a mandate can prevent a future mandate-authorized interaction; it cannot invalidate, freeze, redirect, or delay any ZKA note or exit. ZKM RETIRE concerns remaining mandate authority and is not the ZKA custody exit.

zka:bundle:v1 compatibility proof. The JSON interface in §5.4, its version literal, substantiveness rule, verifier algorithm, error taxonomy, and all existing serialized instances are unchanged. The direct-settlement input is a separate target method and is never parsed by verifyComplianceBundle. Every payment-only zka:bundle:v1 instance remains byte-for-byte valid, unknown ZKM carriage is not implied, and no envelope version bump is warranted. A future recursive/bundle path requires an explicitly specified member, registry binding, equality checks, atomic target transition, and compatibility decision; until then it is non-conforming.

The executable reference boundaries are contracts/ethereum/ZKMAtomicSettlement.sol and src/zka-sdk/src/zkm/mod.rs. The Solidity contract fixes separate immutable spend-conjoined and mandate-only verifier lanes, authenticates both verifier/VK-hash pairs through the target registry at construction, and uses one cross-lane mandate-nullifier mapping; the SDK model stages a cloned state and publishes it only after every effect succeeds. They define the adapter contract, not a claim that every existing ZKA pool deployment has enabled ZKM.

5.5 Predicate Model

A predicate is an open-source compliance circuit that proves exactly one property (e.g. "holder is not on an OFAC SDN list", "holder is over an age threshold"). Predicates are published by independent, permissionless predicate providers.

Proof-type registry is non-exclusive. ZKC's Well-Known Identifier Registry and §3.1–3.6 schemas are references, not a closed allowlist. No type or provider is privileged or required, a verifier's trustPolicy MAY accept predicates outside the registry, and the protocol defines no mechanism for a canonical predicate set. ZKC well-known type IDs are permitted but never required, consistent with Safeguard 1.

5.6 Credential Lifecycle

Status: normative by reference to ZKC. The binding, bundle, and predicate model (§5.2–5.5) are normative in this specification. ZKC owns the issuance, revocation, and accumulator-witness mechanics: the concrete issuer-bootstrapping and accumulator-witness-distribution protocols are specified normatively by the ZKC Specification §4 (Attestation Layer) and the ZKC Issuer Adapter Protocol v1 (ZKC/docs/zkc_issuer_adapter_protocol_v1.md, with schemas/issuer-metadata.schema.json and a machine-checked conformance kit). ZKA references that protocol rather than redefining it.

Issuance / Issuer Bootstrapping. A ZKC credential is issued by a credential issuer (e.g. a KYC provider) binding an attestation to the holder's cbk via holderCommitment = H("zkc/binding", cbk). The issuer never learns the holder's ZKA address or spending key. The issuer-bootstrapping protocol (issuer registration, issuer key publication, holder ↔︎ issuer attestation exchange) is REQUIRED for a conforming ZKC deployment and is specified normatively by ZKC (ZKC Specification §4 and the ZKC Issuer Adapter Protocol v1); ZKA references it rather than redefining it.

Revocation. Credential validity is tracked by a cryptographic accumulator. A predicate proof MUST demonstrate non-revocation by proving a current accumulator witness for the credential. Revocation MUST NOT be able to affect ZKA note validity (Safeguard 3) — a revoked credential causes counterparty refusal only.

Accumulator Witness Update Distribution. On any revocation the accumulator changes and all non-revoked holders need an updated witness. A conforming deployment MUST provide a witness-update distribution channel that does not deanonymize holders (e.g. publish epoch accumulator deltas that holders apply locally). The concrete distribution protocol is Draft.

5.7 Structural Requirements

The following are REQUIRED for a complete deployment. The note discovery, key custody, and key rotation/recovery requirements are normative; delegated proof generation remains Draft where marked.

5.8 Aztec Composability

When deployed as an aztec.nr contract, an identity binding proof (§5.2) is composable with private DeFi within a single Aztec transaction via the kernel circuit call stack (see §8.4). Credential non-revocation checks run against Aztec public state without revealing which credential is checked.


6. Selective Disclosure

6.1 Balance Proof Circuit (zka_balance)

Proves that note.value is a genuine integer within a published [low, high] interval, without revealing the exact value and without consuming the note (no nullifier is emitted). The circuit supports three comparison modes, canonicalized to a single saturating-interval encoding so that a solvency proof, a limit proof, and a range proof are all instances of the same statement rather than three unrelated ones. A balance proof is an atomic single-property disclosure and is subject to Safeguard 2.

interface BalanceProof {
  proof: UltraHonkProof;
  publicInputs: {
    merkleRoot: Hash;          // [0] ZKA state tree anchor (note membership; the note is NOT consumed)
    assetCommitment: Hash;     // [1] identifies the asset type the statement is about
    thresholdOrLower: Field;   // [2] canonical interval LOW bound
    upperBound: Field;         // [3] canonical interval HIGH bound
    proofType: Field;          // [4] comparison mode: 0 = value >= lower, 1 = value <= upper, 2 = range
    context: Hash;             // [5] verifier/session replay binding
  };
}

Comparison modes and the canonical saturating-interval encoding: proof type 0 is a solvency proof (value >= threshold), published as the interval [threshold, 2^64 - 1]. Proof type 1 is a limit proof (value <= threshold), published as [0, threshold]. Proof type 2 is a range proof (lower <= value <= upper), published as [lower, upper] with both bounds free. Both published bounds MUST lie in the u64 balance domain [0, 2^64), and a verified proof establishes that note.value is an integer within the published interval, a subset of [0, 2^64) — not merely that some field-arithmetic relation is satisfied.

Normative requirements:

Non-normative warning: context binding prevents replay of a proof across different verifier/session contexts; it does not prevent reuse of the same proof within one context, since no nullifier is emitted and the note is not consumed. A static, per-verifier context — rather than a fresh value per verification session — degrades replay protection from per-session granularity to per-verifier granularity, and the circuit has no way to detect that this has happened: the non-zero check accepts any fixed non-zero value indefinitely.

6.2 Bilateral AFP Reputation History (zka_reputation)

The bounded zka_reputation prototype supplies the candidate ZKA-family circuit seam for AFP v0.3.12-draft §14.6. It proves an exact count and cumulative value for a clean, finally settled, single-value-domain witness bound to one agreement, one counterparty, an externally supplied history head, and a fresh AFP request context. The subject handle is derived from the prover's private AFP-KDC per-entity seed under an agreement-specific scope; no stable holder commitment, address, proof key, or portable positive score is exposed.

The public context is proof-bound, but the circuit itself checks only context != 0; AFP or the verifier MUST derive it from the authenticated current request and compare it exactly, with stale or repeated counters rejected externally, to provide freshness and replay protection.

The complete circuit statement and witness definition, identifier encoding, Poseidon2 transcript, known-answer vector, and prototype limitations are normative in zka_reputation.md. That document also records a hard integration boundary: neither AFP nor ZKA yet defines the versioned signed attachment or canonical AFP-atom-to-leaf projection needed to authenticate this circuit's root. Until AFP publishes that adapter contract and cross-language KATs, this prototype MUST NOT authorize an afp:limit-adjustment:v1 or be described as an operational afp:reputation-proof. The circuit cannot authenticate a prover-supplied root or remember a previously accepted counter, and a proof never changes note validity, custody, transfer, withdrawal, collateral, or any other state by itself.


7. Bridge Circuits

7.1 Ethereum Bridge

Import/export between the ZKA shielded pool and Ethereum. Solidity verifier auto-generated via nargo codegen-verifier (UltraVerifier.sol). Verifier gas target: <300k gas per verification. Export (withdrawal) is unconditional per Safeguard 5.

A conforming ZKA Ethereum bridge contract MUST satisfy the following invariants:

  1. Ownerless and non-upgradeable. No owner, no pause(), no upgradeTo(), no setOperator(), no setVerifier(), no rescue(), no migrate(), no proxy slot, no selfdestruct, no constructor delegatecall.
  2. Immutable configuration. Asset address, verifier addresses, operator address (if any), liveness parameters, initial root, and the deployment-binding context MUST be declared immutable and set in the constructor.
  3. Append-only mutable state. Root index, total appended commitments, and last-root-submitted timestamp MUST be strictly monotonic. Consumed nullifiers MUST transition only from false to true. Accepted roots MUST be removed only by deterministic window pruning.
  4. Root advancement MUST be gated by a root-update proof verifying that the new root is the correct Poseidon2 incremental fold of the deterministic interleave of on-chain deposit-event commitments and blob-published transfer-announcement commitments into the prior root. The proof system MUST also establish that its private blob-commitment list is the canonical list encoded by the blob identified under invariant 6; the legacy zka_root_update circuit (§3.3) does not establish that relation on its own.
  5. Permissionless force-exit path. When operator silence exceeds a published immutable threshold, any address MUST be able to advance the root and call withdrawal. The contract MUST NOT distinguish operator and non-operator callers for withdrawal.
  6. EIP-4844 blob data availability. Internal-transfer NoteAnnouncements (§5.7) MUST be carried in EIP-4844 blobs referenced by the same transaction that advances the root. The bridge MUST obtain the blob versioned hash through BLOBHASH, bind it to the root-update proof's public inputs, and verify the proof relation required by invariant 4 so the folded leaf list equals the ordered commitment projection decoded from the canonical announcement log. Blob sidecars provide public retrieval only for the network's blob-availability window; a conforming deployment MUST durably archive the canonical payload before expiry, and holders MUST retain the witness data required by §7.1.5. The versioned hash and root-update event alone are not enough to reconstruct expired blob contents.

Historical APL deployment material is outside this public reference specification. The immutable Base APLBridge predates this correction and does not satisfy invariants 4 and 6 because it neither executes BLOBHASH nor proves the blob-to-private-leaf relation; it MUST NOT be used as a conforming reference implementation or receive new escrow.

7.1.1 Bridge State (normative)

A conforming bridge MUST hold, on-chain, all state needed for any holder to exit without any off-chain operator service:

State Description Mutability
Canonical append log Note commitments in deterministic settlement order with zero-based leaf indices (§2.4). Carried as contract storage plus CommitmentAppendEvents. Append-only
Accepted root set Recent Merkle roots per the deployment's accepted-root window (§2.4): latest N roots (N >= 1), a block-height/epoch/wall-clock window, or both. Window parameters MUST be immutable constructor configuration. Deterministic window pruning only
Nullifier set Consumed nullifiers. false → true only
Verifier references Addresses of the withdrawal verifier and (where root advancement is SNARK-gated, invariant 4) the root-update verifier. immutable, or version-pinned by immutable configuration
Escrow balances Per-asset collateral backing the shielded pool's circulating supply. The bridge MUST maintain escrow[asset] >= circulatingSupply[asset] (solvency invariant). Changed only by deposits and withdrawals
Deployment context The deployment-binding context hash (chain, contract, asset-domain, circuit-domain) that withdrawal proofs MUST bind via their context public input. immutable

No other party — operator, indexer, balance server, prover — holds authoritative state. Anything they hold MUST be reconstructable from the bridge's events (§7.1.3) and MUST NOT be required for exit.

7.1.2 Force-Exit Path (normative)

Force-exit is the ordinary withdrawal, exercised permissionlessly. It is an unconditional withdrawal path, not an operator-mediated rescue path: there is no separate rescue circuit, no operator co-signature, no rescue court, and no admin intervention point.

The submitted proof is the standard zka_withdrawal proof (§3.2), whose public inputs are (nullifier, merkleRoot, amount, asset, externalRecipient, context). Per Safeguard 5 (§1.6), no compliance predicate, credential, or binding proof is an input, and the bridge MUST NOT add one.

A conforming bridge MUST process a withdrawal in this order:

  1. Root acceptance. merkleRoot MUST be in the accepted root set; otherwise reject with UNKNOWN_ROOT or STALE_ROOT (§7.1.4).
  2. Nullifier freshness. nullifier MUST NOT be in the nullifier set; otherwise reject with NULLIFIER_ALREADY_SPENT.
  3. Asset support and context binding. asset MUST be a configured asset and context MUST equal the deployment context; otherwise reject with UNSUPPORTED_ASSET / INVALID_CONTEXT.
  4. Proof verification. The proof MUST verify against the immutable withdrawal verifier over exactly the §3.2 public-input vector; otherwise reject with INVALID_PROOF.
  5. Escrow sufficiency. Available escrow for asset MUST cover amount; otherwise reject with INSUFFICIENT_ESCROW. (Under the solvency invariant of §7.1.1 this cannot occur absent a bridge defect; the check is defense-in-depth and MUST still be explicit.)
  6. Effects before interactions. The bridge MUST record the nullifier and decrement supply/escrow accounting before transferring funds to externalRecipient.

No challenge period. For a pure on-chain shielded pool, the accepted root history and the nullifier set are the complete audit surface: a withdrawal is valid iff the proof verifies against an accepted root and the nullifier is fresh. A conforming bridge MUST NOT impose a challenge, delay, or dispute window on shielded-pool exits. Challenge periods exist to police off-chain data withholding (state channels, optimistic rollup state) and are out of scope for this path.

Permissionlessness. The withdrawal entry point MUST be callable by any address, MUST NOT distinguish operator and non-operator callers, and — together with invariant 5 — MUST remain exercisable when the operator is silent: after the published immutable liveness threshold, any address can advance the root with a root-update proof satisfying invariants 4 and 6 and then withdraw.

7.1.3 Root Publication Model (normative)

Root advancement and commitment-append metadata MUST be observable from settlement-chain execution history. Blob-backed announcement payloads MUST additionally remain retrievable from consensus blob sidecars during the network availability window and from the deployment's durable archive afterward:

7.1.4 Failure Codes (normative)

Withdrawal rejections MUST be machine-readable (e.g. Solidity custom errors) and distinguish at minimum:

Code Condition Class
UNKNOWN_ROOT merkleRoot never accepted by this bridge Retryable after re-proving against an accepted root
STALE_ROOT merkleRoot was accepted but has aged out of the window Retryable: regenerate the Merkle path against a current accepted root and re-prove
NULLIFIER_ALREADY_SPENT nullifier already recorded Terminal for this note
INVALID_PROOF Proof fails verification Retryable after correcting the witness
UNSUPPORTED_ASSET asset not configured at deployment Terminal for this bridge instance
INVALID_CONTEXT context does not bind this deployment Retryable with the correct context
INSUFFICIENT_ESCROW Escrow for asset cannot cover amount Retryable; indicates a bridge solvency defect that MUST be observable

A stale or unknown root MUST NOT invalidate the note: the note remains spendable, and the holder retries with a fresh proof. No failure code, and no party, can transition a note itself to an invalid state — only nullifier consumption by the holder's own proof does that (Safeguard 3).

7.1.5 Client-Retained Witness Data (normative)

Force-exit is only credible if holders can construct the withdrawal witness with every operator service unavailable. A conforming client/SDK MUST retain, locally or holder-controlled:

A conforming SDK MUST be able to produce a valid withdrawal proof from this retained data plus public L1 history alone. When witness data is missing, diagnostics MUST be deterministic and machine-readable (missing plaintext, missing/stale path, stale root, spent nullifier), so a holder can distinguish recoverable from unrecoverable states. Retained data MUST NOT expose spending keys to any deployment service.

7.1.6 Relationship to the Aztec Portal Path (informative)

The Aztec native deployment (§8) settles L2 → L1 withdrawals through the Aztec Outbox and an ownerless per-asset portal contract (ADR-0002). That path consumes Aztec kernel-proven L2→L1 messages; it does not use the §3.2 standalone withdrawal proof, this section's bridge state, or the §3.3 root-update circuit. The two exit paths are deliberately separate mechanisms satisfying the same Safeguard 5 obligation; conformance to one does not imply conformance to the other.

7.2 Namada Bridge

Noir package Circuit ID Proves
bridge_namada_import NAMADA_IMPORT_V1 Canonical MASP source nullifier consumed once by an atomic native spend + value-equal ZKA note correctly created (proof + VP obligation)
bridge_namada_import_v2 NAMADA_IMPORT_PUBLIC_AMOUNT_V2 Inactive profile: V2 nullifier/asset plus a ZKA note whose value equals the exact public u64 amount
bridge_namada_import_confidential_v1 NAMADA_IMPORT_CONFIDENTIAL_AMOUNT_V1 Inactive profile: V1 nullifier/asset plus the same confidential value committed twice, to the importer and to the bridge escrow
bridge_namada_export NAMADA_EXPORT_V1 ZKA nullifier valid + MASP note correctly created

Namada bridge proofs use the canonical Noir / Barretenberg UltraHonk / BN254 circuit stack. The Rust crate src/zka-bridge-namada is the settlement integration boundary: for V1 and legacy profiles it handles MASP/ZKA translation, IBC request and packet handling, VP state/event access, ordered public inputs, proof requests, and verifier envelopes. V2 excludes IBC packets, JSON/RPC payloads, relayer assertions, and bridge-local models as authority; its only eligible authority is the authenticated native transaction and execution frame described below. The SDK adapter src/zka-sdk/src/bridges/namada.rs wires the V1/legacy bridge surface into the common SettlementBridge interface. Any retained Groth16 code is non-canonical migration or test support; conforming Namada bridge deployments MUST verify the NAMADA_IMPORT_V1 and NAMADA_EXPORT_V1 UltraHonk/BN254 proof artifacts identified in §9.3. This deployment requirement intentionally excludes the inactive V2 profile below: its committed artifacts are reproducibility and conformance evidence, not an activated verifier target.

7.2.1 VP State and Atomic Native Import (normative)

The conformance table describes the combined proof and settlement obligation, not a guarantee of the standalone Noir circuit. A conforming Namada bridge VP MUST maintain the following consensus-authenticated state; relayer assertions, RPC responses, and bridge-local models MUST NOT substitute for it.

State Description Mutability
Consumed MASP-source-nullifier set Exact canonical 32-byte Namada MASP source nullifiers accepted for import (ADR-0008), checked against the native spend consumed in the same transaction. A previously consumed source MUST NOT mint again, including through another import profile. false → true only in accepted canonical state; no reset or deletion to permit reuse
Consumed ZKA-nullifier set ZKA notes consumed on export, checked before releasing backing. false → true only in accepted canonical state
ZKA commitment tree and accepted roots Authenticated output insertions and roots in deterministic settlement order (§2.4). Append-only commitments; deterministic accepted-root window pruning only
Escrow and circulating supply Per-canonical-asset backing and outstanding ZKA bridge liability. The VP MUST enforce escrow[asset] >= circulatingSupply[asset] after every accepted transition. Imports increase both by the same exact amount; exports decrease both by the same exact amount; checked arithmetic, no unmatched delta
Verifier/profile and deployment context Consensus-authorized, version-pinned verifier selection and chain/domain context; callers cannot select an unrelated verifier or supply authoritative context. Only explicitly authorized profile/context transitions; replay protection MUST survive them

Atomic-native-MASP-spend mandate. Every accepted import MUST include a native MASP spend validated by Namada's native MASP validity authority in the same transaction as the ZKA import. The proof statement and the VP MUST both bind the same source root, source commitment, canonical source nullifier, canonical asset, exact value, destination ZKA commitment, chain/domain context, and owner authorization. This is a combined native-spend-proof and ZKA-proof obligation: source membership, nullifier derivation, and ownership may be established by the native MASP proof, but the VP MUST authenticate that proof's execution and enforce its linkage to the ZKA proof and authorized destination in the same native frame. Merely accepting two unrelated proofs is insufficient. Private fields need not be disclosed; their equality MUST be cryptographically enforced through the selected profile, not inferred from caller metadata. An implementation unable to authenticate any required binding MUST reject before changing state.

The native debit, allocation to bridge backing, ZKA output value, escrow increase, and circulating-supply increase MUST be equal for the same canonical asset. Native source consumption, bridge source-nullifier recording, backing/liability deltas, output insertion, replay state, and success event MUST commit atomically or all remain unchanged. Rejection and rollback MUST leave no partial mint, consumed-source record, or success event; canonical finality governs acceptance. A finalized consumed source MUST never become reusable, while rollback of unfinalized execution rolls back the entire transition together.

Deprecated alternative. The self-contained authenticated-MASP-root plus membership-witness variant is deprecated and MUST NOT be accepted as an alternative to the atomic native spend. Even authenticated membership or correct nullifier derivation proves neither a native debit nor one-time consumption. Unauthenticated membership, a historical nullifier observation, or an off-chain assertion likewise cannot authorize an import.

Implementation boundary. The five-input NAMADA_IMPORT_V1 Noir circuit binds declared canonical nullifier limbs, the ZKA output commitment, asset commitment, and verifier-derived context; it does not independently prove a native spend, source membership, source ownership, or source/output value equality. Its artifacts alone therefore do not satisfy this mandate and MUST NOT enable imports. These normative obligations do not change that circuit ABI, activate a verifier, or expand the protocol-family registry. The separately defined V2 profile below remains inactive under ADR-0012 and MUST satisfy all required native bindings before activation; this section does not widen its immutable encoding or disclosure rules.

7.2.2 Native Public-Amount Import V2 (inactive)

NAMADA_NATIVE_PUBLIC_AMOUNT_IMPORT_V2 is a distinct, immutable application-level profile for the initial native Namada import bundle. It is not legacy V1 and is not an ADR-0009 generic host-profile version. It publicly discloses the canonical spend nullifier, canonical asset, exact u64 base-unit amount, ZKA output commitment, chain/context/profile, and transaction/time/ backing destination. Its public audit record carries explicit profile version 2. It therefore MUST NOT be described as providing end-to-end amount privacy: those fields permit correlation with deposits, withdrawals, balances, timing, and the backing destination. Shielded recipient and output- opening randomness, and later internal shielded transfers, do not restore boundary amount privacy. V2 is fail-closed until an upstream native host verifier/profile can authenticate its same-transaction native MASP spend, exactly one bridge allocation, exactly one ZKA output/tree insertion, exactly one ZKA liability increase, destination authorization, exact accounting, and finality. The public amount, native MASP debit, bridge allocation, private ZKA note value, escrow increase, and liability increase MUST all equal the same exact non-zero u64 base-unit amount for the same canonical asset. An accepted V2 frame MUST atomically consume its canonical source nullifier exactly once, apply equal escrow and ZKA-liability deltas, insert the bound output, update replay and tree state, and emit one canonical native V2 event in the same finalized native write-log transaction. That event MUST contain the complete public boundary tuple above, explicit profile version 2, and exact escrow and liability accounting deltas. A NativeImportV2AuditRecord may be committed only after activation and native acceptance, atomically with that event, and MUST NOT substitute for it. Rejection or rollback MUST leave all of those effects unchanged. These eligibility, disclosure, replay/rollback, event, and activation rules are normative; any change requires a new profile identifier and version.

The reference implementation hard-codes its V2 production activation constant to false, causing the production entrypoint to reject before reading or staging transition state. Constructing native-frame authority is not an activation mechanism. A separately reviewed release may change this gate only after the native hook, exact event, in-VP verifier, governance authorization, and conformance evidence are complete.

The canonical event kind is zka.import_public_amount_v2; it has one payload attribute containing lowercase 0x hexadecimal for exactly 538 bytes. The durable NativeImportV2AuditRecord value stores the same raw bytes. Its normative field/type order is:

event_schema_version:u32, profile_id:bytes32, profile_version:u32,
native_transaction_id:bytes32, masp_transaction_id:bytes32,
consensus_height:u64, consensus_time_seconds:u64, consensus_time_nanos:u32,
chain_id:bytes34, context:bytes32, masp_nullifier:bytes32,
masp_asset_identifier:bytes32, zka_asset_commitment:bytes32,
public_amount:u64, zka_output_commitment:bytes32,
backing_destination:bytes20, escrow_pre:u128, escrow_post:u128,
escrow_delta:i128, liability_pre:u128, liability_post:u128,
liability_delta:i128, allocation_id:bytes32, tree_pre_root:bytes32,
tree_root:bytes32

The schema version is 1 and the profile version is 2. Except for the already-canonical raw byte arrays (including the 34-byte ContextV1 chain ID), all unsigned integers use their full-width big-endian encoding and both signed deltas use full-width 16-byte two's-complement big-endian encoding. Each delta MUST be positive and equal public_amount; its corresponding post-state MUST equal pre-state plus that amount. JSON, decimal strings, base64, host-endian or variable-width integers, field reduction, reordered/omitted/extra fields, or a different byte sequence are invalid. The native hook MUST emit the exact stored payload bytes from the same finalized write-log transaction.

Defining this inactive extension does not advance or alter the supported protocol-family tuple for epoch zka:protocol-family:2026-09-10.1. V2 has no registry verifier target and cannot be advertised as settlement support within that epoch.

7.2.3 Native Confidential-Amount Import V1 (inactive)

NAMADA_NATIVE_CONFIDENTIAL_AMOUNT_IMPORT_V1 ("Confidential V1", ADR-0013) is a separate immutable application-level profile for a confidential native Namada import bundle. It is not a revision of V2, not legacy V1, and not an ADR-0009 generic host-profile version, and it does not alter V2 in any respect: V2 remains the explicitly public-amount profile and MUST NOT be described as amount-private because Confidential V1 exists.

Confidential V1 keeps the settled amount out of every protocol-visible field it introduces. The amount is a private circuit witness; it is absent from the profile's public inputs, its native event, its bridge accounting keys and values, and its audit record. Its ZKA statement is a same-value double issuance: the identical hidden value is committed twice under the identical public asset_commitment, once to the importer as zka_output_commitment and once to the bridge escrow as zka_escrow_commitment, each an ordinary canonical ZKA note commitment blinded independently. Confidentiality depends on that blinding: conforming provers MUST sample each randomness independently from a cryptographically secure source per import, never derive it from the amount, and never share it between the two notes.

On the Namada side an eligible frame is fully shielded: exactly one native MASP spend, exactly one shielded escrow output, no converts, no transparent inputs or outputs, and a Sapling value balance of exactly zero. This is the inverse of V2, which requires a transparent escrow output carrying the exact base units.

Because no publishable amount exists, conservation is structural rather than arithmetic. For each canonical asset the bridge maintains two hash chains — one for the escrow leg, one for the ZKA liability leg — plus an outstanding-import count. An accepted import appends the identical step to both legs, so the invariant escrow_chain == liability_chain is a public, amount-free witness that every outstanding ZKA liability has exactly one escrow entry behind it, in the same order, for the same asset, at the same committed value. That one-to-one, value-equal backing is how Confidential V1 discharges the §7.2.1 escrow[asset] >= circulatingSupply[asset] obligation: it establishes the inequality structurally rather than by comparing published integers, which this profile has none of. No accounting key holds an amount, a balance, or a running total; the outstanding count is a count, never a sum.

An accepted frame MUST atomically consume its canonical source nullifier exactly once across all import profiles, extend both conservation legs by the same step, insert the bound output, update replay and tree state, and emit one canonical native confidential event in the same finalized native write-log transaction. Rejection or rollback MUST leave all of those effects unchanged.

Confidential V1 is amount confidentiality, not anonymity. Timing and transaction-graph position, the number of imports per asset, the canonical asset identity, and escrow-leg activity all remain observable and correlatable; implementations MUST state those residual risks and MUST NOT describe the profile as anonymous or unlinkable. This ADR covers the import direction only: no confidential export exists, so no end-to-end confidential round trip may be claimed.

Confidential V1 is fail-closed. Beyond V2's native hook and in-VP verifier requirements it has two additional gates with no V2 equivalent: authenticated evidence that the sole shielded output pays the configured bridge escrow address, and proof — inside the same frame and without disclosing the value — that the shielded escrow output carries exactly the value opened by zka_escrow_commitment. Namada's MASP binding signature already conserves that value without revealing it, and the circuit already proves the two ZKA notes carry the same value; what is missing is the link between MASP's Jubjub/BLS12-381 value commitment and the BN254 escrow-mirror note. A design in which the verifier learns the value in order to check equality does not satisfy that gate, because a validity predicate's inputs are replicated chain data. The reference implementation hard-codes its activation constant to false and seals its frame authority against both open capabilities separately. The canonical event kind is zka.import_confidential_amount_v1, whose single payload attribute is lowercase 0x hexadecimal for exactly 686 bytes; ADR-0013 fixes the normative field order. Defining this inactive profile does not advance the supported protocol-family tuple for epoch zka:protocol-family:2026-09-10.1; it has no registry verifier target and cannot be advertised as settlement support.

7.3 Penumbra Bridge

Noir package Circuit ID Proves
bridge_penumbra_import PENUMBRA_IMPORT_V1 Penumbra note consumed + ZKA note correctly created
bridge_penumbra_export PENUMBRA_EXPORT_V1 ZKA note consumed + Penumbra note correctly created

Translates between Penumbra value commitments and ZKA note commitments across field boundaries. Penumbra bridge proofs use the canonical Noir / Barretenberg UltraHonk / BN254 circuit stack. The Rust crate src/zka-bridge-penumbra is fresh settlement integration code for Penumbra RPC/view/custody boundaries, asset/nullifier mapping, transaction plans, receipt/event parsing, and Noir proof artifact loading. It is not the removed legacy Halo2/Pallas Penumbra bridge crate and MUST NOT depend on halo2_proofs 0.3 or zka-circuits. The SDK adapter src/zka-sdk/src/bridges/penumbra.rs wires Penumbra into the common SettlementBridge interface.

7.4 Optional external shielded-pool settlement (deployment note)

Distinct from the ZKA bridge circuits above (§7.1–§7.3), the native ZKA/Pay pool profile (§3.4), and Aztec-native deployment (§8), a conforming deployment MAY delegate settlement to an external, immutable, operator-free shielded pool holding a non-freezable asset. ZKA's own transfer circuits (§3) do not run in this mode; the host pool provides the shielded value transfer, and ZKA contributes only the additive ZKA/Coord (§4), ZKC (§5), and AFP federation layer off-chain. Identity therefore binds at the session/context level (§5.4), because host-pool notes are not ZKA notes.

This optional delegation mode demonstrates settlement-rail independence. Namada and Penumbra are likewise optional interoperability paths. The strategic flagship is the Aztec-native, owner-direct ZKA/Pay profile, subject to ADR-0010; this strategic designation is neither an executable deployment target nor a production certification. The Base WETH9 ZKA/Pay profile remains interim and non-production. AFP's signed USD obligation and exact wei settlement quote remain binding wherever that profile is used.


8. Aztec Native Deployment

Aztec-native, owner-direct ZKA/Pay is ZKA's strategic flagship deployment profile, subject to ADR-0010. Strategic selection does not enable execution: an exact target must independently pass its integration, capability and deployment gates. Production additionally requires official V6 resolution, qualification, independent audit and explicit activation. Aztec native is a first-class deployment target for ZKA notes and transfers. ZKA/Coord v2 conformance is separate: an Aztec implementation MUST publish a manifest-bound execution profile and demonstrate the §4 claim with its own conformance evidence; kernel composition alone is insufficient.

The Aztec L1 portal is an Ethereum ingress/egress boundary, not a private bridge: deposit and withdrawal amounts are public on Ethereum, and timing, fees, and receipt metadata remain residual public metadata. Private ZKA transfers occur within the Aztec domain; no cross-domain privacy claim follows from the portal.

8.1 ZKA Transfer as Aztec Private Function

#[private]
fn transfer(
    input_note: ZKANote,
    output_note: ZKANote,
    spending_key: SpendingKey,
    merkle_path: MerklePath
) {
    // ZKA note -> Aztec note stored in Aztec's note hash tree
    // Nullifier managed by Aztec's indexed nullifier tree
    // Kernel circuit handles proof composition
}

ZKA notes become Aztec notes. The Aztec-native ZKA/Pay profile uses the §2.3 canonical commitment and canonical asset-id projection, with active values constrained to 1..=u128::MAX. Every deposit, transfer, withdrawal, popped input value, and change output MUST be range-checked before conversion from Field to u128; aggregate conservation overflow fails closed. A cross-profile transfer or adapter MUST reject a value that exceeds its destination profile's published maximum before narrowing.

Aztec's note hash tree replaces the protocol-level Merkle tree from §2.4. A v1 Aztec-native ZKA/Pay spend uses Aztec's app-siloed nullifier key and compute_note_nullifier; it MUST NOT additionally compute, reveal, or bind the standalone §2.3 computeNullifier result. Aztec membership witnesses, note-tree positions, nullifiers, kernel proofs, and verification keys are target-specific. Aztec's indexed nullifier tree provides uniqueness and double-spend prevention for the Aztec target only. Aztec contract code MUST NOT introduce pausable, upgradeable, or administrative paths over note validity (Safeguard 3).

8.2 ZKA/Coord via Aztec Kernel Circuits

An Aztec Coord profile MAY represent a step as a private function call and use kernel proof composition. The profile circuit/adapter MUST still emit the canonical Coord v2 receipt, bind the exact Aztec program and artifact identity, enforce the workflow's per-step predicate/topology, and satisfy the same verifier-selected policy and completion checks. Until that profile passes the Coord v2 conformance gate, it MUST be labelled non-conforming experimental. The active Aztec workspace contains no built-in Coord contract: consumers use the canonical SDK verification flow and may delegate an Aztec proof only through an independently registered profile, exact artifact identity, and target-specific verifier implementation. Off-chain proof/VK material is never an Aztec fallback.

8.3 Key Mapping

ZKA Key Aztec Key
masterSeed App-siloed nullifier secret nsk_app
spendingKey (sk) H("zka/spending", nsk_app)
viewingKey (vk) H("zka/viewing", spendingKey)
credentialBindingKey (cbk) H("zkc/credential", nsk_app) — sibling of spendingKey
address H("zka/address", viewingKey)

8.4 Composable ZKC Compliance

On Aztec, a single transaction can atomically combine a shielded ZKA transfer (§8.1) and an identity binding / predicate proof (§5) via the kernel circuit call stack, without revealing individual claims. Non-revocation checks run against Aztec public state without revealing which credential is checked.

8.5 Cross-Environment Interoperability

Standalone EVM and Aztec-native deployments share only the items their exact manifests declare shared: the BN254/Poseidon2 field and hash parameters, the §2.3 canonical note-commitment formula, the canonical asset-id projection, the zka/pay/interaction-context/v1 interaction-context profile, and the declared receipt-profile fields. The latter are exactly interaction-context, recipient-output-note-hash, contract-instance-address, and receipt-profile-version, with their pinned schema, field encoding, profile version, and domain separator. This declares shared receipt semantics and encodings; it does not claim that every field value is equal across targets. In particular, a contract-instance address is target-specific, and equality of an interaction context or output-note commitment is claimed only when the manifest identifies the same input under the declared semantics. A conforming implementation of the Aztec-native payment-receipt profile MUST carry these fields in a separate recipient-encrypted Aztec private log; interaction_context MUST NOT be added to a note field, public state, public log, public function argument, nullifier preimage, or withdrawal input. The receipt-log domain separator MUST be distinct from note-announcement and withdrawal messages. The current synthetic production-forbidden fixture does not establish payment-receipt implementation or production support.

Equal canonical note commitments, asset commitments, or declared receipt fields do not make other settlement state equal. Aztec nullifiers use aztec-app-siloed-v1; standalone §2.3 nullifiers, membership witnesses, state-tree positions, proof objects, and verification keys are target-specific. A proof object is target- and VK-bound: an EVM UltraHonk proof is not assumed to verify in an Aztec kernel, or vice versa. A manifest claiming cross-target spend continuity MUST identify an explicit once-only export/import or settlement adapter; equal note hashes alone never authorize independent spending on both targets. An adapter must enforce the destination profile's value bound and reject an out-of-range value before narrowing.

8.6 Relationship to Aztec Primitives

ZKA deliberately shares Aztec's cryptographic substrate — BN254, Poseidon2, UltraHonk/Barretenberg, and Noir — so that the Aztec-native deployment delegates to Aztec's primitives rather than reimplementing them. This subsection makes the delegation boundary explicit: what ZKA stops carrying on Aztec, and what it continues to add. It is descriptive, not a new normative requirement.

Delegated to Aztec (native deployment). In an aztec.nr deployment, the host provides the following and ZKA does not carry its own:

ZKA mechanism (spec) Provided on Aztec by Notes
Note commitment + Merkle tree (§2.3–§2.4) Aztec note hash tree ZKA notes become Aztec notes; the depth-32 Poseidon2 tree is replaced by Aztec's append-only note hash tree
Nullifier set (§2.3, §11.5) Aztec indexed nullifier tree Non-membership and double-spend prevention are Aztec's
Host proof composition Aztec kernel circuits Kernel aggregation is usable only inside an explicit Coord v2 profile; it does not itself establish §4 semantics
Key hierarchy (§2.2) Aztec key derivation app-siloed nsk_app is the AFP-KDC seed; sk and cbk derive as siblings (§8.3)
Proof system / field / hash (§2.1, §10) Aztec native (Honk, BN254, Poseidon2) Identical by construction under the active compatibility boundary in §2.1

On Aztec, the ZKA settlement layer can delegate state primitives to the host. The Coord v2 layer does not reduce to naming conventions: it remains the profile-bound receipt, workflow, policy, and conformance contract defined in §4. ZKA does not claim an improvement over Aztec's host primitives.

Additive — no Aztec-native equivalent. The following are ZKA/ZKC contributions that Aztec does not provide and that remain ZKA's own in every deployment, including the Aztec-native one:

  1. ZKC compliance / attestation (§5). The seed-sibling credential binding (cbk = H("zkc/credential", seed), §2.2), the ZKA-note ↔︎ ZKC-holderCommitment identity-binding circuit (§5.2), selective-disclosure predicates (§5.5, §6), the revocation accumulator and credential lifecycle (§5.6), and the zka:bundle:v1 compliance envelope (§5.4). On Aztec these are implemented as contract logic using Aztec's app-siloed keys (§5.8), but Aztec specifies no equivalent compliance-credential system.
  2. Freedom Safeguards as invariants (§1.6). Operator-blindness, unconditional exit, no admin/upgrade/pause path over note validity, and verifier accountability. These constrain how Aztec may be used rather than duplicating an Aztec feature: §8.1 requires that Aztec contract code introduce no pausable/upgradeable/admin path over note validity (Safeguard 3) — a discipline Aztec's contract model does not impose by default.
  3. Cross-environment compatibility (§8.5). ZKA pins the note commitment scheme, asset-id encoding (ADR-0003), and domain separators so manifests can identify the exact shared commitment, asset-projection, and receipt semantics across environments that use the shared BN254 field. This is a compatibility contract at the circuit/field/hash level. It does not claim equal nullifiers, membership witnesses, state-tree positions, proof objects, or verification keys; nor does it claim that one note simultaneously occupies an Aztec tree and an external bridge tree. Each environment keeps its own state. Cross-target spend continuity requires an explicit once-only export/import or settlement adapter (§8.5); the bridge circuits (§7) are such target-specific settlement mechanisms where applicable.
  4. Settlement-rail independence. ZKA's note/nullifier/proof model is specified independently of any settlement rail, so the same model runs Aztec-native, against a standalone EVM bridge (§7.1), or with no chain at all on an operator-run platform. Aztec's primitives are inseparable from the Aztec rollup; ZKA's are not bound to any one host.

Scope summary. On Aztec specifically, ZKA's value is the additive set above — ZKC compliance, the Freedom Safeguards, and cross-environment note compatibility — layered on delegated Aztec machinery. ZKA's rail-independent primitives (its own tree, nullifier set, recursion plumbing, and the standalone verifier path) are justified by the non-Aztec deployments, not by the Aztec one. Aztec is the strategic flagship deployment profile under the gates stated in ADR-0010; that status does not make it production-certified. This subsection records the boundary between delegation and addition explicitly.


9. Circuit Specifications

9.1 Transfer Circuit

Parameter Value
Proving system UltraHonk (Barretenberg)
Curve BN254
Target proving time <5s (consumer hardware, browser WASM)
Proof size ~2 KB
Solidity verification gas <300k gas
Verification time <50ms (native), <100ms (browser)

9.2 Coordination Circuit

Parameter Value
Receipt public inputs 30 fields
Final recursive public inputs 33 fields
Maximum predecessor fan-in 4
Maximum recursive depth 32
Maximum unrolled proof nodes 4096
Proving time / memory / proof size / gas Unclaimed until the §4 conformance benchmark gate publishes reproducible measurements

9.3 Circuit Identifiers

const CIRCUIT_IDS = {
  // Core circuits — Barretenberg/UltraHonk/BN254
  TRANSFER_V1:            "zka:transfer:ultrahonk:bn254:v1",
  WITHDRAWAL_V1:          "zka:withdrawal:ultrahonk:bn254:v1",
  ROOT_UPDATE_V1:         "zka:root_update:ultrahonk:bn254:v1",
  COORD_RECEIPT_V2:       "zka:coord:receipt:ultrahonk:bn254:v2",
  COORD_PROFILE_V2:       "zka:coord:profile:ultrahonk:bn254:v2",
  COORD_RECURSIVE_V2:     "zka:coord:recursive:ultrahonk:bn254:v2",
  BALANCE_PROOF_V1:       "zka:balance:ultrahonk:bn254:v1",
  REPUTATION_V1:          "zka:reputation:ultrahonk:bn254:v1",
  TRANSFER_MULTIASSET_V1: "zka:transfer:multiasset:ultrahonk:bn254:v1",

  // Immutable Base WETH9 ZKA/Pay pool profile
  PAY_DEPOSIT_V1:         "zka:pay_deposit:ultrahonk:bn254:v1",
  PAY_TRANSITION_V1:      "zka:pay_transition:ultrahonk:bn254:v1",
  PAY_BATCH_V1:           "zka:pay_batch:ultrahonk:bn254:v1",
  PAY_WITHDRAWAL_V1:      "zka:pay_withdrawal:ultrahonk:bn254:v1",

  // Bridge circuits
  NAMADA_IMPORT_V1:       "zka:bridge:namada:import:ultrahonk:bn254:v1",
  NAMADA_IMPORT_PUBLIC_AMOUNT_V2: "zka:bridge_namada_import:ultrahonk:bn254:public-amount:v2",
  NAMADA_IMPORT_CONFIDENTIAL_AMOUNT_V1: "zka:bridge_namada_import:ultrahonk:bn254:confidential-amount:v1",
  NAMADA_EXPORT_V1:       "zka:bridge:namada:export:ultrahonk:bn254:v1",
  PENUMBRA_IMPORT_V1:     "zka:bridge:penumbra:import:ultrahonk:bn254:v1",
  PENUMBRA_EXPORT_V1:     "zka:bridge:penumbra:export:ultrahonk:bn254:v1",

  // Credential / ZKC integration circuits
  CREDENTIAL_BINDING_V1:  "zka:zkc:binding:ultrahonk:bn254:v1",
  TRANSFER_BINDING_V1:    "zka:zkc:transfer_binding:ultrahonk:bn254:v1",

  // Selective disclosure circuits
  AGE_THRESHOLD_V1:       "zka:disclosure:age_threshold:ultrahonk:bn254:v1",
  RANGE_MEMBERSHIP_V1:    "zka:disclosure:range_membership:ultrahonk:bn254:v1",
  SET_MEMBERSHIP_V1:      "zka:disclosure:set_membership:ultrahonk:bn254:v1",
};

Predicate circuits are NOT enumerated here: per Safeguard 1 the protocol carries no canonical predicate set. Predicate circuit identifiers are assigned by their independent providers and selected by a verifier's trustPolicy.

9.4 Noir Project Structure

noir/
├── Nargo.toml                        # Workspace root (Noir v1.0, pinned)
├── zka_lib/                          # Shared primitives (Poseidon2, key derivation, Merkle)
├── zka_transfer/                     # ZKA/Pay transfer
├── zka_withdrawal/                   # Bridge withdrawal
├── zka_root_update/                  # Bridge root advancement (§3.3)
├── zka_balance/                      # Balance proof (selective disclosure)
├── zka_reputation/                   # Bounded bilateral AFP reputation history (§6.2)
├── zka_coord_v2/                     # Shared Coord v2 receipt constraints
├── zka_coordination_v2/              # Coord v2 canonical receipt validator
├── zka_coordination_v2_profile/      # Profile/policy-bound execution adapter
├── zka_coordination_v2_recursive/    # Sound bounded DAG composition
├── zka_coord_v2_sample_application/  # Profile-adapter conformance example
├── zka_credential/                   # ZKC identity binding (§5.2)
├── bridge_namada_import/
├── bridge_namada_import_v2/          # Inactive public-amount profile V2
├── bridge_namada_import_confidential_v1/  # Inactive confidential-amount profile V1
├── bridge_namada_export/
├── bridge_penumbra_import/
└── bridge_penumbra_export/

10. Proof Format

10.1 UltraHonk Proof (Barretenberg)

interface UltraHonkProof {
  proof: Uint8Array;           // Serialised UltraHonk proof bytes
  publicInputs: Field[];       // BN254 scalar field elements
  verificationKey: Uint8Array; // Serialised verification key
}

Proofs are generated by Barretenberg (bb prove) or NoirJS (noir_js + WASM). Verification is performed on-chain by the auto-generated UltraVerifier.sol Solidity contract.

10.2 Solidity Verifier Interface

interface IUltraVerifier {
  function verify(
    bytes calldata proof,
    bytes32[] calldata publicInputs
  ) external view returns (bool);
}

The verifier is generated per-circuit via nargo codegen-verifier. Each circuit has its own deployed UltraVerifier instance. Verifier contracts MUST be non-upgradeable and admin-free (Safeguard 3).


11. Security Considerations

11.1 Cryptographic Assumptions

Assumption Implication if Broken
Discrete log (BN254) Spending key recovery
Poseidon2 collision resistance (BN254 field) Note forgery / identity binding forgery
UltraHonk soundness (Barretenberg) Invalid proofs accepted
Noir ACIR compilation correctness Circuit constraints deviate from spec
Accumulator security Revoked credentials appear valid
Master seed secrecy Full compromise of both ZKA and ZKC identities

11.2 Implementation Requirements

Requirement Rationale
Constant-time nullifier queries Prevent timing attacks
Secure randomness for note creation Prevent commitment grinding
Atomic nullifier recording Prevent double-spend race conditions
Merkle root recency checks Prevent stale state attacks
Pinned Noir + Barretenberg versions Prevent breaking changes in proof format
No admin/pausable/upgrade paths over notes Enforce Safeguard 3
Unconditional unshield/withdraw Enforce Safeguard 5

11.3 Privacy Limitations

Limitation Mitigation
Timing correlation Randomized delays, batching
Transaction graph (long-term) Dummy transactions
Note value distribution Standardized denominations
Metadata leakage Encrypted memos
Cross-chain correlation Bridge batching, timing randomization
Disclosure accumulation over time Minimal disclosure default (Safeguard 2), per-session fresh binding

11.4 Bridge Security

Risk Mitigation
Bridge contract compromise Multi-sig governance of bridge liquidity only — never of note validity
Cross-chain replay Chain-specific context binding
Asset mismatch Strict asset ID verification
Aztec kernel upgrade Version-pinned circuit IDs

11.5 Nullifier Storage Requirements

Nullifier storage MUST provide atomic append with at-least-once delivery guarantees. Event-sourced stores (e.g. KurrentDB/EventStoreDB) satisfy this; the nullifier event log doubles as a compliance audit trail. In Aztec native deployments, Aztec's indexed nullifier tree provides these guarantees natively.

11.6 Post-Quantum Considerations (Draft)

BN254 discrete-log and the UltraHonk proving system are not post-quantum secure. A future revision will specify a PQ migration path (hash-based commitments are Poseidon2 and remain conjecturally PQ-resistant for collision; key derivation and proof system are the migration surface). This remains open work for a future revision.


12. Conformance

12.1 ZKA Conformance Levels

Level Requirements
ZKA-Core Transfer proofs, nullifier tracking, canonical Merkle append/root handling, note discovery, non-custodial key ownership, Safeguards 3 & 5
ZKA-Coord Core + Coord v2 profile-bound receipt, recursive DAG proof, verifier policy, and completed conformance gate (§4)
ZKA-Full Coord + viewing keys + selective disclosure
ZKA-MultiChain Full + Namada/Penumbra/Aztec bridges
ZKA-Compliant MultiChain + ZKC integration (§5), all six Safeguards
ZKA-Aztec Full Aztec native deployment plus an independently conforming manifest-bound Coord v2 Aztec profile and the value, nullifier, and interoperability boundaries of §§2.3, 8.1, and 8.5

The ZKA-Coord level requires the exact soundness claim and evidence gate of §4. The reference profile is bounded to fan-in 4, depth 32, and 4096 unrolled proof nodes; no target may claim unbounded depth or infer conformance from host proof composition alone. Another proving mechanism can conform only through an explicit manifest-bound profile with equivalent receipt, workflow, policy, data-flow, completion, and adversarial-verification evidence.

Bridge-deploying conformance. Any conformance level claimed by a deployment that operates a ZKA Ethereum bridge MUST additionally deploy a zka_root_update verifier (§3.3) referenced by the bridge contract as VERIFIER_ROOTUPDATE, and the bridge MUST satisfy all six invariants of §7.1.

Aztec conformance. A ZKA-Aztec deployment MUST publish a manifest that declares nullifier_semantics = aztec-app-siloed-v1 and the selected value width and maximum (u128 and u128::MAX for the Aztec-native ZKA/Pay profile). The manifest MUST state that nullifier, witness, proof, and verification-key equality with EVM is not claimed; it MUST enumerate any shared commitment, asset-projection, and receipt fields by exact encoding and domain. It MUST identify an explicit adapter when claiming cross-target spend continuity.

12.2 ZKC Integration Conformance Levels

ZKC conformance levels are owned by the ZKC Specification v0.4 §9.1. The core ladder is strictly monotonic — ZKC-Core → ZKC-Standard → ZKC-Full (Full is the highest core level) — and ZKC-Agent is an orthogonal extension profile, not a level above ZKC-Full. ZKC-Core includes KYC proofs, the credential rebind circuit (zkc:proof:credential_rebind:v1), and basic verification. This spec does not re-declare a parallel Integration-* taxonomy; a deployment integrating ZKC MUST conform to the ZKC v0.4 §9.1 level it claims, and tooling MUST treat the level name as ZKC-namespaced rather than inferring rank from the ZKA taxonomy. The ZKA-side requirements that gate the ZKA-Compliant level (§12.1) are: shared key hierarchy (§2.2), identity binding circuit (§5.2), compliance bundle envelope (§5.4), and all six Freedom Safeguards (§1.6).

A deployment claiming ZKA-Compliant MUST satisfy all six Freedom Safeguards (§1.6); they are not gated by conformance level.

12.3 Test Vectors

Test vectors are provided in the companion zka-test-vectors repository and generated over the BN254 scalar field using Poseidon2. The published vector set includes the identity-binding and combined transfer-and-binding circuits.


Appendix A: UltraHonk Proof Format

interface UltraHonkProof {
  proof: Uint8Array;            // ~2 KB serialised proof
  publicInputs: Field[];        // Circuit-specific public inputs (BN254 field elements)
  verificationKey: Uint8Array;  // ~3 KB serialised VK
}

interface RecursiveProofInputs {
  proof: Field[];               // Proof as field array for std::verify_proof
  verificationKey: Field[];     // VK as field array
  publicInputs: Field[];
}

Appendix B: Poseidon2 Parameters (BN254)

Poseidon2 Configuration for ZKA
================================
Field:          BN254 scalar field
                (r = 21888242871839275222246405745257275088548364400416034343698204186575808495617)
State width:    4
Rate:           3 field elements
Capacity:       1 field element
Full rounds:    8
Partial rounds: 56
S-box:          x^5
Length IV:      message_length * 2^64 in state[3]
Library:        noir-lang/poseidon v0.3.0 Poseidon2::hash,
                backed by Noir/Aztec/Barretenberg Poseidon2

Appendix C: ZKA-ZKC Integration

C.1 Current ZKC v0.4 relationship

ZKC Specification v0.4 is the current ZKC integration target. It retains linkable zkc:proof:v1 and adds zkc:proof:v2 with anonymous and scoped presentation profiles. ZKA uses the same cbk = H("zkc/credential", masterSeed), scoped domain separator, anonymous zero sentinel, Poseidon2/BN254 field encoding, registry-selected predicate identifiers, replay context, issuance, and revocation rules. Predicate versions are independent of the carrying envelope version: notably, zkc:proof:selective_disclosure:v1 is carried in a v2 anonymous or scoped envelope. ZKA adds only the note-ownership conjunction in zka_transfer_binding_v2; it does not redefine ZKC predicate verification.

ZKA-side reconciliation: zka:bundle:v1 wraps both ZKC envelope families and dispatches verification by the exact (envelopeVersion, presentationProfile, proofType) tuple. Payment bindings accept v1/linkable or v2/scoped only; payment-free work attestations may carry v2/anonymous because identity remains bound to the authenticated session-context field projection. The legacy v1 circuit and wire shape remain accepted unchanged.

Precedence: Per ZKC v0.4 §1.4, where ZKC and ZKA §1.6 differ in wording on a Freedom Safeguard, the safeguard intent as stated in ZKA §1.6 governs. ZKA §1.6 is the authoritative source for safeguard intent; ZKC mirrors it.

Required ZKC properties:

  1. Non-exclusive proof types (§5.5). ZKC's well-known identifiers and reference schemas MUST NOT privilege or require one type or provider.
  2. Open-source predicates (Safeguard 4). Predicate circuits MUST be auditable and versioned, with default refusal of opaque predicates.
  3. Conformance hierarchy. The ladder is ZKC-Core → ZKC-Standard → ZKC-Full; ZKC-Agent is an orthogonal extension profile.

Notation reference. Both specifications reference AFP-KDC for hashing, domain separation, and seed-to-key derivation rather than a ZKA-versioned derivation. AFP-KDC v1.0.2 §A.3 defines the deployed Blake2b-512 little-endian field-tag derivation. The derivation facts cited above (cbk = H("zkc/credential", masterSeed), H("zkc/binding", cbk), Poseidon2/BN254) are defined by AFP-KDC §A.6 and are cited here, not redefined.

Credential rebind reference. ZKC v0.4 §4.5 defines zkc:proof:credential_rebind:v1 as a ZKC-Core circuit that proves holder-controlled migration from oldHolderCommitment to newHolderCommitment without revealing either seed, either cbk, any ZKA address, any spending key, or payment activity. ZKA does not duplicate that circuit. ZKA's obligations are to keep note migration private via self-transfer batching (§5.7), keep any issuer-retained old-to-new holder-commitment mappings sealed and minimized (§5.7), and ensure rebind proofs never affect ZKA note validity, custody, transferability, withdrawal, nullifier semantics, or revocation semantics.


License

This specification is released under the Apache 2.0 License.

Patent Non-Assertion Covenant

The Apache 2.0 patent grant (License §3) covers patent claims necessarily infringed by a contribution alone or by its combination with the Work it was submitted to. An independent implementation of this specification — one written from the specification text without deriving from any reference code — falls outside that scope, and so is not clearly covered by that grant. The following covenant, modeled on the Open Web Foundation Agreement (OWFa) 1.0 non-assert and the patent commitment of the Community Specification License 1.0, closes that gap. By submitting a contribution to this specification (for example, a pull request modifying this document), each contributor accepts and makes this covenant for every version of this specification that incorporates its contribution.

Each contributor to this specification irrevocably covenants, on behalf of itself and its successors and assigns, not to assert any Essential Claims against any party for making, having made, using, selling, offering for sale, importing, or distributing a Conformant Implementation of this specification. Essential Claims are patent claims owned or controlled by the contributor — now or in the future, including claims later acquired — that are necessarily infringed by implementing the required portions (those designated MUST, REQUIRED, or SHALL) of a version of this specification to which the contributor contributed. "Necessarily infringed" follows the W3C Patent Policy definition of Essential Claims: it applies only where no commercially reasonable non-infringing alternative exists, and it excludes claims covering implementation choices this specification leaves open and technologies this specification merely references rather than describes in detail. A Conformant Implementation is any implementation that conforms to those required portions — whether or not it derives from any contributor's code or specification text; where an implementation forms part of a larger work, this covenant extends only to the portions that so conform. This covenant is royalty-free and worldwide, and supplements — never limits — the rights granted under the Apache 2.0 License.

This covenant runs with the Essential Claims: it is intended to bind any future owner, assignee, or exclusive licensee that acquires the right to enforce them, and a contributor that transfers a patent containing Essential Claims satisfies this obligation by notifying the transferee of this covenant. The covenant is suspended with respect to any party that asserts a patent infringement claim alleging that a Conformant Implementation of this specification infringes that party's patents (excluding claims brought defensively in response to a prior such assertion by a contributor or any successor or assignee bound by this covenant), for as long as that assertion is maintained.


ZKA: Provable confidentiality for autonomous agents.