From fa640101367c9381324e332c12ddc6e5c1250d46 Mon Sep 17 00:00:00 2001 From: DROOdotFOO Date: Sat, 16 May 2026 12:14:56 +0200 Subject: [PATCH 01/14] Add ERC: ZK Compliance Oracle --- ERCS/erc-draft_xochi-zkp.md | 685 ++++++++++++++++++++++++++++++++++++ 1 file changed, 685 insertions(+) create mode 100644 ERCS/erc-draft_xochi-zkp.md diff --git a/ERCS/erc-draft_xochi-zkp.md b/ERCS/erc-draft_xochi-zkp.md new file mode 100644 index 00000000000..608f563f15d --- /dev/null +++ b/ERCS/erc-draft_xochi-zkp.md @@ -0,0 +1,685 @@ +--- +eip: TBD +title: Zero-Knowledge Compliance Oracle +description: On-chain ZK compliance verification without revealing transaction data +author: DROO (@DROOdotFOO), Bloo (@bloo-berries), Merkle Bonsai (@Jabher) +discussions-to: https://ethereum-magicians.org/t/erc-zero-knowledge-compliance-oracle/28543 +status: Draft +type: Standards Track +category: ERC +created: 2026-04-07 +requires: 165 +--- + +## Abstract + +A standard interface for on-chain verification of regulatory compliance (AML, sanctions screening, anti-structuring) using zero-knowledge proofs. Users generate proofs client-side that attest to compliance with jurisdiction-specific thresholds without revealing transaction amounts, counterparty identities, or screening details. Verifiers confirm proof validity on-chain. No trusted third party or TEE is required. + +## Motivation + +Public blockchains force a binary choice between transparency and privacy. Transparent execution (Uniswap, CoW Protocol) exposes trades to billions in cumulative MEV extraction. Privacy tools (Tornado Cash) have been sanctioned for lacking compliance mechanisms. + +Existing approaches to compliant privacy fall short: + +- **View keys** (Railgun, Panther): Trade privately, then reveal raw transaction data to auditors on request. This leaks the data: it is delayed transparency. +- **TEE-based compliance** (various): Rely on hardware trust assumptions that have been broken repeatedly (SGX side channels, key extraction). +- **Compliance-by-exclusion** (Privacy Pools): Prove you're NOT in a bad set. Doesn't prove you ARE compliant with specific jurisdiction rules. + +This ERC defines a standard where compliance is proven cryptographically at transaction time. The proof commits to screening results, jurisdiction thresholds, and provider attestations. Regulators verify a proof. They never see the underlying data. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. + +### Terminology + +- **providerSetHash**: A commitment to the specific set of screening providers and their weights used for a particular compliance proof. Included in each attestation for retroactive verification. +- **providerConfigHash**: A hash of the global provider weight configuration published by the oracle administrator. Versioned on-chain; weight changes push a new entry to the config history. +- **attestation TTL**: The duration (in seconds) for which a compliance attestation remains valid after on-chain recording. Expired attestations remain queryable via `getHistoricalProof()` but are not considered valid by `checkCompliance()`. + +### Proof Types + +Implementations MUST support the following proof types. Each type corresponds to a separate ZK circuit with its own verification key. + +All proof types include `submitter` as a public input; implementations MUST enforce +`submitter == msg.sender` at submission time. + +| Type ID | Name | Circuit | Public inputs | Private inputs | +| ------- | ----------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | +| 0x01 | Compliance | compliance | jurisdiction_id, provider_set_hash, config_hash, timestamp, meets_threshold, submitter | signals, weights, weight_sum, provider_ids, num_providers | +| 0x02 | Risk Score | risk_score | proof_type (threshold/range), direction, bound_lower, bound_upper, result, config_hash, provider_set_hash, submitter | signals, weights, weight_sum, provider_ids, num_providers | +| 0x03 | Pattern | pattern | analysis_type, result, reporting_threshold, time_window, tx_set_hash, submitter, settlement_root | amounts, timestamps, num_transactions | +| 0x04 | Attestation | attestation | provider_id, credential_type, is_valid, credential_root, current_timestamp, submitter | credential_attribute, expiry_timestamp, merkle_index, merkle_path | +| 0x05 | Membership | membership | merkle_root, set_id, timestamp, is_member, submitter | subject_salt, merkle_index, merkle_path | +| 0x06 | Non-membership | non_membership | merkle_root, set_id, timestamp, is_non_member, submitter | low_leaf, low_leaf_salt, low_index, low_path, high_leaf, high_leaf_salt, high_index, high_path | +| 0x07 | Compliance Signed | compliance_signed | jurisdiction_id, provider_set_hash, config_hash, timestamp, meets_threshold, signer_pubkey_hash, chain_id, oracle_address, submitter | signals, weights, weight_sum, provider_ids, num_providers, signature, pubkey_x, pubkey_y | +| 0x08 | Risk Score Signed | risk_score_signed | proof_type, direction, bound_lower, bound_upper, result, config_hash, provider_set_hash, signer_pubkey_hash, chain_id, oracle_address, submitter | signals, weights, weight_sum, provider_ids, num_providers, signature, pubkey_x, pubkey_y, signed_timestamp | +| 0x09 | Compliance Multi-Signed | compliance_multi_signed | jurisdiction_id, provider_set_hash, config_hash, timestamp, meets_threshold, threshold_m, signer_pubkey_hash_0..4, chain_id, oracle_address, submitter | per-slot signals/weights/weight_sums/pubkey_x/pubkey_y/signature (5 slots each) | + +Notes on the proof type semantics: + +- **Attestation (0x04).** The leaf in the per-provider credentials Merkle tree is `leaf_hash_value(credential_hash)`, where `credential_hash = H(DOMAIN_CREDENTIAL, provider_id, submitter, credential_type, credential_attribute, expiry_timestamp)`. The hash binds the credential to a specific submitter at issuance time; cross-submitter forgery is not possible without breaking Pedersen preimage resistance. `credential_root` references a per-provider tree registered via `publishCredentialRoot`; the on-chain `providerId` recorded against the root must match the `provider_id` in the proof's public inputs. + +- **Membership (0x05) and Non-membership (0x06).** The leaf is `leaf_hash_subject(value, set_id, salt)`. For membership, `value` is the submitter's address (the leaf is computed from the public `submitter` input + private `subject_salt`). For non-membership, `value` is the bracketing tree entry (`low_leaf` / `high_leaf`), and the proof asserts `low_leaf < submitter < high_leaf` using full-width Field comparison (no u64 ceiling). Tree publishers MUST sort leaves by `value`; the circuit additionally requires `high_index == low_index + 1` to prevent an attacker from skipping a real intermediate entry. + +- **Pattern (0x03).** The `analysis_type` field selects the analysis kind: 1 = anti-structuring, 2 = velocity, 3 = round-amounts. Implementations that depend on a specific analysis (e.g., a settlement registry requiring anti-structuring) MUST verify the `analysis_type` field; storing only the `result` boolean is insufficient. The `settlement_root` public input is opaque to the circuit (set to 0 for standalone use, or to a downstream consumer's declarative binding value). Consumers that need to bind a pattern proof to a specific downstream state (e.g., the sub-settlements of a particular trade) MUST recompute the expected `settlement_root` from their own state and assert equality, and SHOULD mark each consumed pattern proof to prevent reuse across multiple bound contexts. + +- **Risk Score (0x02).** Validators MUST reject trivially-true claims (`bound_lower = 0` for direction GT, `bound_lower >= MAX_RISK_SCORE_BPS` for direction LT, full-domain ranges). The `meetsThreshold` boolean stored on the attestation reflects only the cryptographic `result` field; integrators querying RISK_SCORE attestations should also verify the bounds match their integration's expectations. + +- **Provider-signed variants (0x07 Compliance Signed, 0x08 Risk Score Signed).** Identical semantics to their unsigned siblings, plus an in-circuit secp256k1 ECDSA verification of a Pedersen digest committing to `(chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`. The provider's pubkey commitment is exposed as `signer_pubkey_hash`; implementations MUST validate it against an on-chain registry. The `chain_id` and `oracle_address` public inputs MUST match `block.chainid` and the consuming Oracle's address: this binds a single provider signature to one deployment so the same signed payload cannot mint attestations across chains or against alternate Oracle deployments. Strict-mode jurisdictions (e.g. US BSA, Singapore) reject the unsigned siblings entirely; permissive jurisdictions accept either form. + +- **Compliance Multi-Signed (0x09).** Extends the signed model to M-of-N. The circuit bundles up to five parallel signer slots; a slot is active iff its public `signer_pubkey_hash` is non-zero. Each active slot independently verifies a secp256k1 signature over a slot-specific Pedersen digest carrying its own `slot_index` (under a distinct `DOMAIN_MULTI_SIGNED_SIGNALS` tag) and independently asserts the per-provider risk score is below the jurisdiction high-risk floor. The Oracle MUST validate each non-zero slot's `signer_pubkey_hash` against the registry, MUST reject duplicate hashes across active slots, MUST enforce `chain_id == block.chainid` and `oracle_address == address(this)`, and MUST enforce `threshold_m >= JurisdictionConfig.minMultiProviderThreshold(jurisdictionId)` (e.g., US BSA and Singapore require M >= 2; permissive jurisdictions accept M >= 1). Forging an attestation under 0x09 requires compromising at least M of the N registered signing keys simultaneously. + +### Verifier Interface + +The verifier routes proof verification to per-proof-type verification contracts. Each circuit produces a separate verifier via the ZK backend (e.g., `bb write_solidity_verifier` for Barretenberg's UltraHonk). + +```solidity +interface IXochiZKPVerifier { + /// @notice Verify a zero-knowledge compliance proof + /// @param proofType The type of proof (0x01-0x09) + /// @param proof The encoded proof data + /// @param publicInputs The public inputs to the verification circuit (packed bytes32 values) + /// @return valid Whether the proof is valid + function verifyProof( + uint8 proofType, + bytes calldata proof, + bytes calldata publicInputs + ) external view returns (bool valid); + + /// @notice Verify a batch of proofs atomically + /// @param proofTypes Array of proof types + /// @param proofs Array of encoded proofs + /// @param publicInputs Array of public input sets + /// @return valid Whether ALL proofs are valid + function verifyProofBatch( + uint8[] calldata proofTypes, + bytes[] calldata proofs, + bytes[] calldata publicInputs + ) external view returns (bool valid); + + /// @notice Get the current verifier address for a proof type + /// @param proofType The proof type (0x01-0x09) + /// @return verifier The verifier contract address (address(0) if not set) + function getVerifier(uint8 proofType) external view returns (address verifier); + + /// @notice Verify a proof against a specific historical verifier version + /// @dev Required for retroactive verification: a proof generated under a prior + /// verifier version must remain checkable after the current verifier has + /// been upgraded. Revoked versions (see Verifier Versioning) MUST revert. + /// @param proofType The proof type (0x01-0x09) + /// @param version The 1-indexed verifier version + /// @param proof The encoded proof data + /// @param publicInputs The public inputs + /// @return valid Whether the proof is valid + function verifyProofAtVersion( + uint8 proofType, + uint256 version, + bytes calldata proof, + bytes calldata publicInputs + ) external view returns (bool valid); + + /// @notice Get the verifier address for a specific historical version + /// @param proofType The proof type (0x01-0x09) + /// @param version The 1-indexed verifier version + /// @return verifier The verifier contract address + function getVerifierAtVersion(uint8 proofType, uint256 version) external view returns (address verifier); + + /// @notice Get the current verifier version for a proof type + /// @param proofType The proof type (0x01-0x09) + /// @return version The current version (0 if no verifier set) + function getVerifierVersion(uint8 proofType) external view returns (uint256 version); +} +``` + +Implementations MUST also implement [ERC-165](./eip-165.md). `supportsInterface(bytes4)` MUST return `true` for `type(IXochiZKPVerifier).interfaceId` and for `type(IERC165).interfaceId`, and `false` for `0xffffffff`. + +### Oracle Interface + +```solidity +interface IXochiZKPOracle { + struct ComplianceAttestation { + address subject; // address that proved compliance (msg.sender at submission) + uint8 jurisdictionId; // jurisdiction (0=EU, 1=US, 2=UK, 3=SG) + uint8 proofType; // which proof type produced this attestation (0x01-0x09) + bool meetsThreshold; // whether the rule was satisfied + uint256 timestamp; // block.timestamp at submission + uint256 expiresAt; // block.timestamp + attestationTTL + bytes32 proofHash; // keccak256(proof, proofType, chainId, oracleAddr) -- see Proof Hash Computation + bytes32 providerSetHash; // hash of providers + weights (COMPLIANCE/COMPLIANCE_SIGNED only; bytes32(0) otherwise) + bytes32 publicInputsHash; // keccak256(publicInputs) + address verifierUsed; // verifier contract address at submission time (TOCTOU-safe) + } + + event ComplianceVerified( + address indexed subject, + uint8 indexed jurisdictionId, + bool meetsThreshold, + bytes32 indexed proofHash, + uint256 expiresAt, + uint256 previousExpiresAt + ); + + event ProviderWeightsUpdated( + bytes32 indexed configHash, + uint256 timestamp, + string metadataURI + ); + + event AttestationTTLUpdated(uint256 oldTTL, uint256 newTTL); + event ConfigRevoked(bytes32 indexed configHash); + event MerkleRootRegistered(bytes32 indexed merkleRoot); + event MerkleRootRevoked(bytes32 indexed merkleRoot); + event ReportingThresholdRegistered(bytes32 indexed threshold); + event ReportingThresholdRevoked(bytes32 indexed threshold); + + /// @notice Submit a compliance proof and record the attestation + /// @param jurisdictionId Target jurisdiction (0=EU, 1=US, 2=UK, 3=SG) + /// @param proofType The proof type for verifier routing (0x01-0x09) + /// @param proof The ZK proof data + /// @param publicInputs Public inputs matching the circuit's pub parameters + /// @param providerSetHash Hash of provider IDs and weights used for screening + /// @return attestation The recorded compliance attestation + function submitCompliance( + uint8 jurisdictionId, + uint8 proofType, + bytes calldata proof, + bytes calldata publicInputs, + bytes32 providerSetHash + ) external returns (ComplianceAttestation memory attestation); + + /// @notice Submit a batch of compliance proofs atomically + /// @dev All entries share `jurisdictionId`. The batch reverts if ANY entry fails + /// verification, validation, or replay checks. Implementations MUST cap the + /// batch size (see Batch verification limits). + /// @param jurisdictionId Target jurisdiction for all entries (0=EU, 1=US, 2=UK, 3=SG) + /// @param proofTypes Proof type for each entry (0x01-0x09) + /// @param proofs ZK proof data for each entry + /// @param publicInputs Public inputs for each entry + /// @param providerSetHashes Provider set hash for each entry + /// @return attestations The recorded compliance attestations, in input order + function submitComplianceBatch( + uint8 jurisdictionId, + uint8[] calldata proofTypes, + bytes[] calldata proofs, + bytes[] calldata publicInputs, + bytes32[] calldata providerSetHashes + ) external returns (ComplianceAttestation[] memory attestations); + + /// @notice Check if an address has a valid (non-expired) compliance attestation + /// @param subject The address to check + /// @param jurisdictionId The jurisdiction to check against + /// @return valid Whether a valid, non-expired attestation exists + /// @return attestation The attestation if valid + function checkCompliance( + address subject, + uint8 jurisdictionId + ) external view returns (bool valid, ComplianceAttestation memory attestation); + + /// @notice Check compliance filtered by proof type + /// @dev Integrators that require a specific proof family (e.g. only signed variants, + /// or only ATTESTATION-backed) MUST use this rather than `checkCompliance()`, + /// since the latest attestation per (subject, jurisdiction) may have been + /// produced by any supported proof type. + /// @param subject The address to check + /// @param jurisdictionId The jurisdiction + /// @param proofType The required proof type (0x01-0x09) + /// @return valid Whether a valid attestation of the specified type exists + /// @return attestation The attestation if valid + function checkComplianceByType( + address subject, + uint8 jurisdictionId, + uint8 proofType + ) external view returns (bool valid, ComplianceAttestation memory attestation); + + /// @notice Retrieve a proof for retroactive verification (proof-of-innocence) + /// @param proofHash The hash of the original compliance proof + /// @return attestation The original attestation record + function getHistoricalProof( + bytes32 proofHash + ) external view returns (ComplianceAttestation memory attestation); + + /// @notice Get the proof type that produced an attestation + /// @dev Equivalent to `getHistoricalProof(proofHash).proofType` but cheaper. + /// @param proofHash The hash of the original proof + /// @return proofType The proof type identifier (0x01-0x09) + function getProofType(bytes32 proofHash) external view returns (uint8 proofType); + + /// @notice Get all attestation hashes for a subject in a jurisdiction + /// @dev Returns an unbounded array. Implementations SHOULD also expose a + /// paginated variant for subjects with large histories. + /// @param subject The address to query + /// @param jurisdictionId The jurisdiction + /// @return proofHashes Array of proof hashes for historical lookup + function getAttestationHistory( + address subject, + uint8 jurisdictionId + ) external view returns (bytes32[] memory proofHashes); + + /// @notice Get the current provider weight configuration hash + /// @return configHash Hash of current provider weights + function providerConfigHash() external view returns (bytes32 configHash); + + /// @notice Get the current attestation time-to-live + /// @return ttl Duration in seconds that attestations remain valid + function attestationTTL() external view returns (uint256 ttl); +} +``` + +Implementations MUST also implement [ERC-165](./eip-165.md). `supportsInterface(bytes4)` MUST return `true` for `type(IXochiZKPOracle).interfaceId` and for `type(IERC165).interfaceId`, and `false` for `0xffffffff`. + +### Jurisdiction Configuration + +Implementations MUST publish jurisdiction thresholds openly. Risk scores are expressed in basis points (0-10000 = 0.00%-100.00%). + +| ID | Jurisdiction | Low (bps) | Medium (bps) | High / Filing trigger (bps) | +| --- | ------------ | --------- | ------------ | --------------------------- | +| 0 | EU (AMLD6) | 0-3099 | 3100-7099 | >=7100 | +| 1 | US (BSA) | 0-2599 | 2600-6599 | >=6600 | +| 2 | UK (MLR) | 0-3099 | 3100-7099 | >=7100 | +| 3 | Singapore | 0-3599 | 3600-7599 | >=7600 | + +### Attestation Lifecycle + +Compliance attestations have a configurable time-to-live (TTL): + +- Default TTL: 24 hours +- Minimum TTL: 1 hour +- Maximum TTL: 30 days +- `expiresAt = block.timestamp + attestationTTL` at submission time + +`checkCompliance()` MUST return `false` for expired attestations. Expired attestations MUST remain retrievable via `getHistoricalProof()` for proof-of-innocence purposes. The TTL is updatable by the oracle administrator via `updateAttestationTTL()`. + +### Provider Weight Publication + +Implementations SHOULD publish provider weights as an on-chain configuration hash. Weight changes MUST emit `ProviderWeightsUpdated` with the new configuration hash, timestamp, and an optional `metadataURI` pointing to the full configuration (e.g., on IPFS or Arweave). + +Provider configuration MUST be versioned. Implementations SHOULD maintain a history of configuration hashes to support retroactive verification: determining which weights were active when a particular proof was generated. Implementations SHOULD support revoking historical configuration hashes when a configuration is discovered to be flawed. The currently active configuration MUST NOT be revocable. + +### Proof Type Routing + +Implementations MUST maintain a registry mapping each proof type to a per-circuit verifier contract. Each ZK circuit (compiled separately) produces its own verification key and verifier contract. The main verifier contract acts as a router: + +1. Caller specifies `proofType` (0x01-0x09) +2. Router looks up the registered verifier for that type +3. Public inputs are decoded from packed `bytes` to `bytes32[]` +4. The per-circuit verifier's `verify(bytes, bytes32[])` is called + +Verifier addresses are updatable to allow circuit upgrades. Implementations SHOULD use a two-step ownership transfer pattern for administrative operations. + +### Verifier Versioning + +Verifier upgrades MUST NOT invalidate proofs that were valid under a prior verifier. An on-chain attestation produced under version $v_n$ records `verifierUsed` at submission time, but a counterparty months later may need to re-run the verification — for example, to recompute proof-of-innocence after a discovered circuit bug or to independently audit a historical attestation. This is impossible if the contract retains only the latest verifier address. + +Implementations MUST maintain an append-only version history per proof type and expose three operations: + +- `getVerifierVersion(proofType)` returns the current version count (1-indexed). +- `getVerifierAtVersion(proofType, version)` returns the verifier contract address at that version. +- `verifyProofAtVersion(proofType, version, proof, publicInputs)` re-runs verification through the historical verifier. + +Implementations MUST support revoking a specific historical version when a verifier is discovered to be unsound. Revocation MUST NOT delete the entry from history (the address remains recoverable via `getVerifierAtVersion`), but `verifyProofAtVersion` against a revoked version MUST revert. Revoking the current (latest) version MUST be forbidden — current revocation must instead proceed by proposing a replacement verifier through the upgrade timelock and then revoking the prior version. + +Revocation MAY have two paths: a delayed path (default) and an immediate path gated behind the GUARDIAN role for cases where a paused proof type needs the revocation locked in before the timelock elapses. The reference implementation uses a 6 h delay for the routine path. + +### Public Input Validation + +Implementations MUST validate public inputs semantically for each proof type before forwarding to the per-circuit verifier. The ZK proof guarantees internal consistency (e.g., that the score was correctly computed from the committed inputs), but the oracle MUST verify that those committed inputs match the expected context (e.g., that the config hash is a known configuration, that the merkle root belongs to a registered set). Without this validation, a valid proof generated for one context can be replayed in a different context. + +Public inputs MUST be 32-byte aligned. Implementations MUST reject `publicInputs` where `length % 32 != 0`. + +The following validation MUST be performed per proof type: + +| Proof Type | Validated Fields | Registry | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------- | +| COMPLIANCE | jurisdiction_id, provider_set_hash, config_hash, meets_threshold | Config hash registry | +| RISK_SCORE | result, config_hash, provider_set_hash | Config hash registry | +| PATTERN | result, reporting_threshold, tx_set_hash != 0 | Reporting threshold registry | +| ATTESTATION | is_valid, credential_root, provider_id | Credential root registry (per-provider) | +| MEMBERSHIP | merkle_root, is_member | Merkle root registry | +| NON_MEMBERSHIP | merkle_root, is_non_member | Merkle root registry | +| COMPLIANCE_SIGNED | jurisdiction_id, provider_set_hash, config_hash, meets_threshold, signer_pubkey_hash, chain_id == block.chainid, oracle_address == address(this) | Config hash + signer-pubkey-hash registries | +| RISK_SCORE_SIGNED | result, config_hash, provider_set_hash, semantic-bound checks, signer_pubkey_hash, chain_id == block.chainid, oracle_address == address(this) | Config hash + signer-pubkey-hash registries | + +### Proof Result Validation + +Each proof type includes a boolean result field (`meets_threshold`, `result`, `is_valid`, `is_member`, `is_non_member`) in its public inputs. A valid ZK proof with a false result means the prover proved they do NOT satisfy the condition (e.g., non-compliant, not a member). Implementations MUST reject proofs where the result field is not `true` (encoded as `bytes32(uint256(1))`). Without this check, a user could submit a cryptographically valid proof of non-compliance and receive a compliant attestation. + +The `providerSetHash` parameter in `submitCompliance()` is semantically meaningful for COMPLIANCE proofs, which include it as a caller-supplied public input. RISK_SCORE proofs also commit to a `provider_set_hash` in their circuit public inputs, but this value is embedded in the proof itself and does not come from the caller parameter. For all non-COMPLIANCE proof types, implementations MUST ignore the caller-supplied `providerSetHash` and store `bytes32(0)` in the attestation to prevent injection of arbitrary values. + +### Validation Registries + +Implementations MUST maintain on-chain registries for values that public inputs are validated against. These registries prevent context-spoofing attacks where a proof generated for one context is submitted in a different context. + +**Config hash registry.** Tracks valid provider weight configuration hashes. New hashes are added when the administrator updates the configuration. Historical hashes SHOULD be revocable (see Provider Weight Publication). The currently active configuration MUST NOT be revocable. Implementations MUST permanently retain revocation status: a previously-revoked config hash MUST NOT be re-registrable, to prevent silent un-revocation. + +**Merkle root registry.** Tracks valid merkle roots for MEMBERSHIP and NON_MEMBERSHIP proofs (typically managed sets such as sanctions lists or whitelists). Roots MUST be registered by the administrator before proofs referencing them can be accepted. Roots SHOULD be revocable when the underlying set is superseded or compromised. + +**Credential root registry (per-provider).** Tracks valid credentials Merkle roots for ATTESTATION proofs, keyed by `provider_id`. Each provider has an authorized publisher EOA, set by the administrator via a separate registration step. The publisher SHOULD publish new credential roots periodically (replacing prior ones). Roots SHOULD have a finite TTL window during which they are accepted; this window allows users with paths against an outgoing root to continue submitting proofs while a new root propagates. Implementations MUST verify the proof's `provider_id` matches the registered `providerId` for the credential root being referenced; otherwise an attacker could reuse another provider's root with a forged `provider_id`. + +**Reporting threshold registry.** Tracks valid reporting thresholds for PATTERN (anti-structuring) proofs. Each jurisdiction defines its own reporting threshold (e.g., $10,000 for US BSA). Thresholds MUST be registered before proofs referencing them can be accepted. + +### Risk Score Computation + +The risk score formula MUST be deterministic and publicly verifiable: + +$$\text{RiskScore}_{\text{bps}} = \frac{\displaystyle\sum_{i=1}^{N} \text{signal}_i \cdot \text{weight}_i}{W} \times 100$$ + +where $\text{signal}_i \in [0, 100]$ are provider screening results, $\text{weight}_i$ are published provider weights, $W = \sum_{i=1}^{N} \text{weight}_i$ is the weight sum, and $N \leq 8$ is the number of active providers. The result is in basis points ($0$-$10000$, i.e., $0.00\%$-$100.00\%$). + +Circuits that accept `weight_sum` as a private input MUST constrain it to equal the actual sum of the `weights` array. Without this constraint, a malicious prover could pass an arbitrary denominator to inflate or deflate the computed score. + +The ZK proof commits to: + +- Signal values (hidden) +- Weights used (public via config_hash, must match published config) +- Resulting score (hidden) +- Whether jurisdiction threshold was crossed (revealed as boolean) + +### Hash Function Requirements + +Circuits MUST use a collision-resistant hash function for all commitments (provider set hashes, config hashes, Merkle trees, credential hashes). The reference implementation uses Pedersen hash, which is efficient in ZK circuits and available in the Noir standard library. + +Pedersen commitments are additively homomorphic over the underlying elliptic curve. This is safe provided: + +1. Hash outputs are used only as opaque commitments compared via equality. +2. No circuit composes hash outputs arithmetically (e.g., `H(x) + H(y)`). +3. All hash calls use fixed-arity inputs to prevent length-extension reinterpretation. + +Implementations MAY migrate to Poseidon2 when high-level APIs stabilize in the circuit language, as Poseidon2 provides stronger random-oracle properties. + +### Merkle Tree Domain Separation + +Implementations MUST use distinct domain tags for leaf and internal-node hashes to prevent the second-preimage attack where an attacker crafts a leaf whose hash collides with an internal node. The reference implementation uses three explicit tags: one for internal nodes, one for set-style leaves bound to `(element, set_id)`, and one for value-style leaves committing a single value (e.g., `credential_hash` in the attestation circuit). + +The fixed-arity Pedersen hash used in the reference implementation does NOT achieve domain separation by input arity alone (e.g., `H([a, b, 0]) == H([a, b])` for the standard pedersen_hash without an explicit length tag). Implementations MUST therefore include an explicit domain tag in the input array. + +### Non-Membership Proof Security + +The non-membership circuit proves that the SUBMITTER is NOT in a sorted Merkle tree by demonstrating adjacency: there exist two consecutive leaves $l$ and $h$ in the tree such that $l < \text{submitter} < h$ AND $\text{high\_index} = \text{low\_index} + 1$. + +The adjacency requirement is critical. Without it, an attacker could pick two non-adjacent tree entries that bracket the submitter, hiding any real intermediate entry that contains the submitter's address. Tree publishers MUST sort leaves by their raw value (the `value` argument to `leaf_hash_subject`). Implementations SHOULD insert sentinel boundary leaves at $0$ and $p-1$ (BN254 prime minus 1) so every submitter has well-defined neighbors. + +Comparison MUST be performed over the full Field range using bit-decomposition (Noir's `Field::lt`). Earlier designs that cast to `u64` and compared as fixed-width integers required additional range checks on all values; the reference implementation uses Field-level comparison to support arbitrary-width identifiers (Ethereum addresses, hashes, etc.) without truncation risk. + +### Submitter Binding + +Implementations MUST bind every proof to its submitter. Each proof type includes `submitter` as a public input that the on-chain validator enforces equal to `msg.sender`. For proofs that prove a fact about a specific party (membership, non-membership, attestation), the proof's leaf format MUST also bind to `submitter` in-circuit so the proof is meaningful only for that submitter: + +- Membership / non-membership: `leaf_hash_subject(value, set_id, salt)` where `value` derives from the relevant party (e.g., `submitter` for membership; the bracketing tree entries for non-membership ordering). +- Attestation: `credential_hash = H(DOMAIN_CREDENTIAL, provider_id, submitter, credential_type, credential_attribute, expiry_timestamp)`, then `leaf_hash_value(credential_hash)`. + +Without this binding, an unauthorized party could submit a proof asserting facts about an arbitrary value and claim the resulting attestation as their own. + +### Retroactive Flagging + +Each compliance proof MUST commit to: + +1. Provider IDs used for screening (committed via providerSetHash) +2. Results returned by each provider at proof time (hidden) +3. The oracle's clearing decision (revealed as meetsThreshold boolean) +4. A timestamp binding the proof to a specific block + +This enables proof-of-innocence: counterparties to retroactively flagged addresses can present the original attestation (retrieved via `getHistoricalProof()`) demonstrating the address was clean at transaction time. The on-chain record is immutable and independently verifiable. + +## Rationale + +**Why client-side computation?** Server-side or TEE-based compliance creates a trusted party that can be coerced, compromised, or surveilled. Client-side ZK proof generation means the raw data never leaves the user's device. The verifier learns only the boolean result. + +**Why published weights?** "Black box" compliance algorithms invite regulatory skepticism and legal challenge. Publishing weights and thresholds makes the system auditable without compromising individual privacy. When enforcement data reveals a provider consistently misses bad actors, the weight adjustment is transparent. + +**Why on-chain attestations?** Off-chain attestations can be forged, lost, or denied. On-chain records are immutable, timestamped, and independently verifiable. This is critical for proof-of-innocence: the proof must be retrievable months or years after the original transaction. + +**Why not Privacy Pools inclusion/exclusion proofs?** Privacy Pools prove set membership ("I'm not in the OFAC set"). This ERC proves compliance with specific rules ("my risk score under jurisdiction X is below threshold Y using providers A, B, C"). Set membership is a subset of what's needed for regulatory compliance. + +**Why attestation TTL?** Compliance status is not permanent. A user who was compliant yesterday may not be compliant today. Screening providers update their data continuously. The TTL forces periodic re-attestation while keeping the window configurable per deployment context. + +**Why nine proof types?** Each proof type maps to a separate ZK circuit with distinct constraint logic. Compliance handles the core risk score check. Risk Score provides standalone threshold/range proofs. Pattern detects structuring behaviors. Attestation verifies credentials from authorized providers. Membership proves inclusion in an authorized set (whitelist). Non-membership proves exclusion from a sanctions list via sorted Merkle tree adjacency. The two single-signer `_signed` variants (Compliance Signed, Risk Score Signed) shadow their unsigned siblings but additionally verify one provider's secp256k1 ECDSA signature over the screening payload in-circuit and bind to (`chain_id`, `oracle_address`). The Compliance Multi-Signed variant (0x09) extends this further to M-of-N: up to five parallel signer slots, each independently signature- and floor-checked, with a runtime `threshold_m` and a per-jurisdiction floor for M. They are separate circuits rather than an oracle-side flag because the signature check materially changes the constraint set: an unsigned proof has no provenance for its `signals[]` private witness, while a signed proof cryptographically attests them. Strict-mode jurisdictions (US BSA, Singapore) accept only the signed forms. This separation keeps individual circuits small and auditable, and lets unsigned-tolerant jurisdictions deploy without paying the signature-verification gas overhead. + +### What this standard does NOT prove + +The single most important caveat for adopters: the cryptographic guarantees in this ERC are about _correct computation_, not about _honest inputs_. Three trust tiers exist across the proof types, and integrators MUST pick the tier that matches their threat model. + +| Tier | Proof types | Who attests the screening signals? | +| ------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------ | +| Self-attested | COMPLIANCE, RISK_SCORE | The submitter. The circuit accepts `signals[]` as a private witness with no signature. | +| Provider-attested | COMPLIANCE_SIGNED, RISK_SCORE_SIGNED | A registered provider, via in-circuit secp256k1 ECDSA over the screening payload. | +| Credential-attested | ATTESTATION (composed with the above) | A registered credential-tree publisher EOA, via Merkle inclusion against a published root. | + +The self-attested tier is useful for jurisdictions that explicitly permit user-asserted compliance (some EU and UK contexts), for fast-path flows where a downstream system performs the honest-signal check, and as a building block in larger composed proofs. A user submitting a self-attested COMPLIANCE proof could in principle pass `signals = [0, ...]` and produce a valid "low-risk" proof regardless of their true screening result; `provider_set_hash` and `config_hash` commit to _which_ providers and weights were used, not to _what_ those providers returned. This is documented as an explicit design tradeoff, not a bug. + +Strict-mode jurisdictions (US BSA, Singapore) MUST reject the self-attested tier — the reference enforces this via `JurisdictionConfig.requireSignedSignals(uint8)`. Permissive jurisdictions MAY accept either tier. Integrators whose threat model includes a dishonest user but a trusted provider MUST require the signed variants. Integrators whose threat model includes a compromised provider key MUST additionally require an ATTESTATION proof against an independently-published credential tree, ideally with an in-circuit signature over the credential root (out of scope for this specification, tracked as future work). + +Implementations SHOULD prominently document this trust model in deployment-facing materials. + +### Related Work + +Several existing and emerging standards address compliance, privacy, or on-chain ZK verification. This ERC differs from each in scope, architecture, or trust model. + +**[ERC-3643](./eip-3643.md) (T-REX).** The ratified compliance token standard for regulated securities, with $32B+ in tokenized assets. ERC-3643 requires identity revelation via ONCHAINID claims verified by trusted issuers. This ERC proves compliance without revealing identity data, provider signals, or transaction amounts. The two standards are complementary: this ERC could serve as a ZK-enhanced identity provider within an ERC-3643 deployment. + +**Privacy Pools (0xbow).** Live on Ethereum mainnet since March 2025. Users prove their withdrawal originates from a "clean" deposit set using ZK proofs, with Association Set Providers (ASPs) maintaining approved deposit lists. The Privacy Pools protocol validates the "prove compliance without revealing data" model. However, set membership is a subset of what regulatory compliance requires. This ERC extends the approach to multi-dimensional compliance: risk scoring, anti-structuring detection, credential verification, and membership/non-membership proofs. + +**[EIP-7963](./eip-7963.md).** An oracle-permissioned [ERC-20](./eip-20.md) that validates token transfers via ZK proofs against off-chain payment instructions (ISO 20022 format), using RISC Zero as the proof system. EIP-7963 gates a single token's transfers through a single oracle with a single proof type. This ERC provides standalone compliance attestations with nine proof types, usable by any contract, and is not gated to token operations. + +**VOSA-RWA.** A compliance-gated privacy token for real-world assets (Draft, 2026). Every token operation requires dual ZK proofs: a compliance attestation (Groth16/BN254, Poseidon hashing) and a transaction conservation proof. VOSA-RWA and this ERC share the "ZK proof for compliance, no PII on-chain" design, but VOSA-RWA embeds compliance into a specific token standard. This ERC is a standalone oracle whose attestations are reusable across protocols. + +**[ERC-7812](./eip-7812.md).** A ZK identity registry using a singleton Sparse Merkle Tree (80-level, Poseidon on BN128) with custom registrars for business logic. Deployed on Ethereum mainnet. ERC-7812 provides a general-purpose private statement registry. This ERC could operate as a compliance-specific registrar within ERC-7812, storing compliance commitments in its Merkle tree. + +**[ERC-8039](./eip-8039.md).** A proof-system-agnostic ZK verification interface for smart accounts (`verifyProof(bytes,bytes) returns (bytes4)`). ERC-8039 standardizes per-relation verifier contracts with a non-reverting return pattern (following [ERC-1271](./eip-1271.md)). This ERC's per-proof-type verifier routing serves a similar verification role but with domain-specific semantics (proof type routing, batch verification, version history). Each generated UltraHonk verifier in this ERC could be wrapped behind an ERC-8039 adapter for smart account integration. + +**[EIP-7702](./eip-7702.md).** Account abstraction via temporary delegation: an EOA can authorize a contract to execute code on its behalf for a single transaction. EIP-7702 interacts with the `submitter == msg.sender` rule in two ways. First, when a 7702-delegated EOA calls `submitCompliance`, `msg.sender` is the EOA address (not the delegated contract), so the attestation correctly binds to the EOA and the `submitter` public input must equal that EOA. Second, a smart-account batcher (using 7702 to wrap multiple operations) can call `submitComplianceBatch` provided every entry's `submitter` public input equals the delegating EOA. Account-abstraction wallets MUST surface the bound `submitter` address to the user before submission, since a malicious dApp could otherwise solicit proofs bound to the wrong address. The same considerations apply to [ERC-4337](./eip-4337.md) paymasters and ERC-1271 contract signers when used as compliance subjects. + +**[ERC-8035](./eip-8035.md) / [ERC-8036](./eip-8036.md) (MultiTrust Credential).** Non-transferable credential anchors with ZK presentation via fixed Groth16 ABI, supporting predicate proofs ("score >= threshold") without revealing raw data. The predicate-proving pattern parallels this ERC's RISK_SCORE proof type. MultiTrust focuses on credential issuance and presentation; this ERC focuses on compliance attestation and retroactive verification. + +**[ERC-1922](./eip-1922.md).** The original zk-SNARK verifier standard (2019, stagnant). ERC-1922 defines a generic interface for on-chain ZK verification with dynamic arrays for cross-scheme compatibility. This ERC supersedes ERC-1922's approach with per-proof-type routing, UltraHonk support, and domain-specific input validation. + +## Backwards Compatibility + +This ERC introduces new interfaces and does not modify existing standards. It is designed to complement [ERC-5564](./eip-5564.md) (stealth addresses) and [ERC-6538](./eip-6538.md) (stealth meta-address registry) for privacy-preserving settlement, but does not depend on them. + +## Test Cases + +The reference implementation includes binary proof fixtures in `test/fixtures/` for the six unsigned proof types. Static fixtures are not provided for the three signed variants (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED, COMPLIANCE_MULTI_SIGNED) because each requires a fresh secp256k1 ECDSA witness; those are exercised end-to-end in the TypeScript SDK consumer tests instead. Each unsigned fixture contains: + +- `proof`: the raw UltraHonk proof bytes (8640 bytes each) +- `public_inputs`: the packed bytes32 public inputs + +| Proof Type | Public Inputs Size | Logical Public Inputs | +| -------------- | -------------------- | -------------------------------------------------------------------------------------------------- | +| COMPLIANCE | 192 bytes (6 inputs) | jurisdiction_id, provider_set_hash, config_hash, timestamp, meets_threshold, submitter | +| RISK_SCORE | 256 bytes (8 inputs) | proof_type, direction, bound_lower, bound_upper, result, config_hash, provider_set_hash, submitter | +| PATTERN | 224 bytes (7 inputs) | analysis_type, result, reporting_threshold, time_window, tx_set_hash, submitter, settlement_root | +| ATTESTATION | 192 bytes (6 inputs) | provider_id, credential_type, is_valid, credential_root, current_timestamp, submitter | +| MEMBERSHIP | 160 bytes (5 inputs) | merkle_root, set_id, timestamp, is_member, submitter | +| NON_MEMBERSHIP | 160 bytes (5 inputs) | merkle_root, set_id, timestamp, is_non_member, submitter | + +All fixtures use Pedersen hash (Noir stdlib) for in-circuit commitments and Merkle tree construction. Fixtures can be regenerated via `scripts/generate-fixtures.sh`. + +### Witness Annex + +The exact Prover.toml inputs used to produce the binary fixtures are reproduced below so other implementations can cross-validate against the same witness. All `submitter` values are `0xdead` and all `timestamp` values are `1700000000` (UNIX seconds, 2023-11-14). Address-style values are packed as field elements; Pedersen hashes on BN254 are reproduced verbatim. + +```toml +# circuits/compliance/Prover.toml +signals = [20, 0, 0, 0, 0, 0, 0, 0] +weights = [100, 0, 0, 0, 0, 0, 0, 0] +weight_sum = 100 +provider_ids = ["1", "0", "0", "0", "0", "0", "0", "0"] +num_providers = 1 +jurisdiction_id = 0 # EU +provider_set_hash = "0x14b6becf762f80a24078e62fc9a7eca246b8e406d19962dda817b173f30a94b2" +config_hash = "0x18574f427f33c6c77af53be06544bd749c9a1db855599d950af61ea613df8405" +timestamp = "1700000000" +meets_threshold = true +submitter = "0xdead" +# Derived risk score: 20 * 100 / 100 * 100 = 2000 bps (below EU 7100 trigger). +``` + +```toml +# circuits/risk_score/Prover.toml +signals = [60, 0, 0, 0, 0, 0, 0, 0] +weights = [100, 0, 0, 0, 0, 0, 0, 0] +weight_sum = 100 +provider_ids = ["1", "0", "0", "0", "0", "0", "0", "0"] +num_providers = 1 +proof_type = 1 # threshold +direction = 1 # GT +bound_lower = 5000 # asserts score > 5000 bps +bound_upper = 0 +result = true +config_hash = "0x18574f427f33c6c77af53be06544bd749c9a1db855599d950af61ea613df8405" +provider_set_hash = "0x14b6becf762f80a24078e62fc9a7eca246b8e406d19962dda817b173f30a94b2" +submitter = "0xdead" +# Derived risk score: 60 * 100 / 100 * 100 = 6000 bps > 5000 -> result=true. +``` + +```toml +# circuits/pattern/Prover.toml (clean anti-structuring) +amounts = [500, 1200, 3000, 7500, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +timestamps = [1700000000, 1700001000, 1700002000, 1700003000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +num_transactions = 4 +analysis_type = 1 # anti-structuring +result = true # clean +reporting_threshold = 10000 # USD-equivalent, US BSA-style +time_window = 3600 +tx_set_hash = "0x2231d26d52515af30cbb6e91834cdb9e3d1d36575f160cbb4f6ebbb3c3dd8dad" +submitter = "0xdead" +settlement_root = "0" # 0 = standalone use (no downstream binding) +``` + +```toml +# circuits/attestation/Prover.toml (KYC credential) +credential_attribute = "999" +expiry_timestamp = 2000000000 +merkle_index = "0" +merkle_path = ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0", + "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"] # 20 levels +provider_id = "42" +credential_type = 1 # KYC basic +is_valid = true +credential_root = "0x24ce58f9ed6ca066d25f66b15b0eb1dccebe6e457f5aa0fcd353d82d539f5ed5" +current_timestamp = 1700000000 # < expiry +submitter = "0xdead" +# credential_hash = H(DOMAIN_CREDENTIAL, provider_id=42, submitter=0xdead, +# credential_type=1, credential_attribute=999, expiry=2000000000) +``` + +```toml +# circuits/membership/Prover.toml (submitter is a member of set 1) +subject_salt = "0" # 0 = public set +merkle_index = "0" +merkle_path = ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0", + "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"] +merkle_root = "0x1d7de002251083fdc312a329d46abde0680cbccc27935c33815c18b1beb3da8c" +set_id = "1" +timestamp = "1700000000" +is_member = true +submitter = "0xdead" +# leaf = leaf_hash_subject(submitter=0xdead, set_id=1, salt=0) +``` + +```toml +# circuits/non_membership/Prover.toml (submitter NOT in set {0x100, 0x10000}) +low_leaf = "0x100" +low_leaf_salt = "0" +low_index = "0" +low_path = ["0x2e3a62a21fa1706df17be5649ad62e45a4dbdbe9a9ce3923058d940cdc6b929d", + "0", "0", "0", "0", "0", "0", "0", "0", "0", + "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"] +high_leaf = "0x10000" +high_leaf_salt = "0" +high_index = "1" # adjacent to low_index +high_path = ["0x0c57a3ac2ba9abef99b6ab714e307311687782f270b6517717e181e5cd50cce5", + "0", "0", "0", "0", "0", "0", "0", "0", "0", + "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"] +merkle_root = "0x138f818fd4f2eec91e4fd93e14bcc47bc06a3ba333e5a2e7795d0beb752d247c" +set_id = "1" +timestamp = "1700000000" +is_non_member = true +submitter = "0xdead" +# Adjacency check: low_leaf (0x100) < submitter (0xdead) < high_leaf (0x10000) +# AND high_index == low_index + 1. +``` + +Signed-variant witnesses (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) are identical to their unsigned siblings in the screening payload, plus `pubkey_x`, `pubkey_y`, `signature`, `signer_pubkey_hash`, `chain_id`, and `oracle_address`. The signature is computed off-chain by the provider over `H_pedersen(chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`. COMPLIANCE_MULTI_SIGNED (0x09) extends this to five parallel signer slots: each active slot supplies its own `(signals, weights, weight_sum, pubkey_x, pubkey_y, signature)` and a non-zero `signer_pubkey_hash`, where each signature commits to a slot-specific Pedersen digest under the `DOMAIN_MULTI_SIGNED_SIGNALS` tag (with embedded `slot_index`). Implementations producing fresh fixtures MUST sample fresh nonces — the reference implementation does this in `test/sdk/` rather than committing a static witness. + +## Reference Implementation + +A reference implementation accompanies this ERC. It consists of: + +- **Solidity contracts**: `src/XochiZKPVerifier.sol`, `src/XochiZKPOracle.sol`, `src/SettlementRegistry.sol`, `src/XochiTimelock.sol` (Foundry, Solidity 0.8.28, Cancun EVM) +- **Noir circuits**: `circuits/` (one per proof type, pinned to nargo 1.0.0-beta.20 via `.tool-versions`) +- **Generated verifiers**: `src/generated/` (UltraHonk verifiers generated by Barretenberg, pinned to bb 4.0.0-nightly.20260120) +- **Test suite**: Solidity tests (unit, fuzz, invariant, integration with real proofs for the 6 unsigned proof types) and circuit tests across all 9 circuits. The signed variants (0x07, 0x08, 0x09) are exercised in the TypeScript SDK consumer tests (`test/sdk/`) which generate a fresh ECDSA witness per run. + +Thanks to Merkle Bonsai (@Jabher) for reviewing the generated UltraHonk verifiers and identifying that the `pairing()` free function could be rewritten in inline Yul to bring all nine per-proof-type verifiers under the [EIP-170](./eip-170.md) 24,576-byte runtime size limit. The reference implementation incorporates the rewrite in `scripts/patch-pairing-yul.sh`, saving ~186 bytes per verifier (and ~800 gas per `verifyProof` call as a bonus) while staying byte-identical to the `bb`-generated semantics on the pairing precompile (`address(0x08)`) input layout. + +## Security Considerations + +**Proof soundness.** The security of the system depends on the ZK proof system used. Implementations MUST use a proof system with at least 128-bit security. Groth16, PLONK, and UltraHonk (Noir/Aztec) are acceptable. + +**Provider collusion.** If all screening providers collude, they could issue false clean signals. Implementations SHOULD require attestations from multiple independent providers and weight them based on enforcement track record. + +**Timestamp manipulation.** Proofs commit to block timestamps. Block proposers control the timestamp, constrained only to be >= the parent block's timestamp. This is acceptable for compliance windows measured in days. Circuits MUST enforce realistic timestamp bounds (e.g., after 2021-01-01 and before year ~36000) to reject obviously invalid values. This applies to both public timestamp inputs (compliance, membership, non-membership) and private transaction timestamps (pattern). + +**Regulatory acceptance.** This standard provides a technical mechanism for ZK compliance. Whether specific jurisdictions accept ZK proofs as sufficient compliance evidence is a legal question, not a technical one. The VARA (Dubai) definition of "anonymity-enhanced crypto" excludes assets with "mitigating technologies" for traceability. This standard provides exactly that technology. + +**Front-running the oracle.** Compliance proofs are generated before settlement. An adversary who observes a proof submission could infer a trade is about to occur. Implementations SHOULD batch proof submissions or submit them as part of the settlement transaction to minimize information leakage. + +**Administrative operations.** Verifier contract updates and provider weight changes are privileged operations. Implementations SHOULD use a two-step ownership transfer pattern (transferOwnership + acceptOwnership) to prevent accidental transfer to an incorrect address. Critical operations (verifier replacement, TTL changes) SHOULD be timelocked in production deployments. Implementations SHOULD further split administrative authority into role classes with bounded blast radius -- for example, a pause-only "guardian" role distinct from registry-mutating and config-mutating roles -- so that a single compromised key cannot both pause and rewrite registries. The reference implementation uses a three-role split (GUARDIAN, REGISTRAR, CONFIG) under a 2-tier selector-gated timelock; see `docs/THREAT_MODEL.md` for the full per-role capability matrix and the timelock-tier mapping. + +**Public input validation.** Implementations MUST validate public inputs for every proof type, not just the primary compliance proof. Without validation, a prover can generate a proof for one context (e.g., a lenient jurisdiction's reporting threshold) and submit it for a different context. Specifically: + +- ALL proof types MUST validate their boolean result field (`meets_threshold`, `result`, `is_valid`, `is_member`, `is_non_member`) equals `bytes32(uint256(1))`. A valid proof with a false result proves non-compliance; accepting it would record a compliant attestation for a non-compliant subject. +- ALL proof types MUST enforce `submitter == msg.sender` to prevent submission front-running. +- COMPLIANCE and RISK_SCORE proofs MUST validate `config_hash` against a registry of known configurations. +- COMPLIANCE proofs MUST validate `jurisdiction_id` and `provider_set_hash` against caller-supplied parameters. +- RISK_SCORE proofs commit to `provider_set_hash` as a public input, binding the proof to a specific set of screening providers. This prevents a prover from fabricating signals from unverified providers. +- RISK_SCORE proofs MUST validate the semantic public inputs (`proof_type ∈ {threshold, range}`, `direction ∈ {GT, LT}`, and bounds) to reject trivially-true claims. For example, a THRESHOLD/GT proof with `bound_lower = 0` proves only that the score is greater than zero, which is uninformative. Validators MUST reject such proofs. +- PATTERN (anti-structuring) proofs MUST validate `reporting_threshold` against a per-jurisdiction registry, enforce `time_window >= MIN_TIME_WINDOW`, and validate that `analysis_type` is one of the supported analyses. Implementations that depend on a specific analysis type (e.g., a settlement registry requiring anti-structuring) MUST verify the `analysis_type` field separately, since the result boolean alone is insufficient. +- ATTESTATION proofs MUST validate `credential_root` against the per-provider credential root registry AND verify the registry's recorded `providerId` matches the proof's `provider_id` public input. Without this cross-check, a proof for one provider's tree could be replayed against another provider's registration. +- MEMBERSHIP and NON_MEMBERSHIP proofs MUST validate `merkle_root` against the generic merkle root registry. +- COMPLIANCE_SIGNED and RISK_SCORE_SIGNED proofs MUST validate `signer_pubkey_hash` against an on-chain registry of authorized provider signing keys, AND MUST validate `chain_id == block.chainid` and `oracle_address == address(this)`. Without the chain/oracle binding, a single provider signature could mint attestations across alternate Oracle deployments (different chain, or a forked Oracle on the same chain). +- Strict-mode jurisdictions SHOULD reject the unsigned siblings entirely and accept only the signed variants. The reference implementation enforces this for US (BSA) and Singapore via `JurisdictionConfig.requireSignedSignals(uint8)`. +- Unknown proof types (outside 0x01-0x09) MUST be rejected. + +**Proof replay prevention.** Proof hashes MUST be keyed on the proof bytes, the proof type, and the deployment context: `keccak256(abi.encodePacked(proof, proofType, block.chainid, address(this)))`. Including `proofType` scopes uniqueness per proof type (identical proof bytes submitted for different proof types are treated as distinct proofs); including `block.chainid` and `address(this)` prevents replay-into-storage from a forked or alternate Oracle deployment on the same or different chain, even if the underlying ZK proof is chain-agnostic. Note this is the on-chain replay guard only; see "Cross-chain replay" below for in-circuit chain binding (relevant to the signed variants). + +**Config and root revocation.** Provider configuration hashes and merkle roots SHOULD be revocable. Without revocation, a discovered-to-be-flawed configuration or a compromised merkle tree remains accepted forever. Implementations MUST NOT allow revoking the currently active provider configuration. Provider configuration history SHOULD be bounded to prevent unbounded storage growth (e.g., 256 entries). + +**Verifier TOCTOU.** Implementations MUST resolve the verifier address once per submission and use it for both proof verification and attestation recording. A time-of-check/time-of-use gap between address resolution and proof verification could allow the recorded `verifierUsed` to diverge from the actual verifier if a verifier upgrade occurs mid-transaction. + +**Batch verification limits.** Implementations MUST enforce a maximum batch size for `verifyProofBatch()` to prevent unbounded gas consumption. The reference implementation uses a limit of 10 proofs per batch, sized to fit comfortably under a 30M-gas mainnet block ceiling (approximately 24M gas at the maximum batch size, with ~5M gas of headroom). Implementations targeting chains with different block-gas budgets SHOULD recalibrate the limit accordingly. + +**Expected gas costs.** UltraHonk verification dominates total transaction cost. The following figures are measured against the reference implementation on the Cancun EVM with real proofs (see `test/GasBenchmark.t.sol`); other proof systems and circuit revisions will differ. + +| Operation | Approx. gas | +| ----------------------------------------------------- | ----------- | +| `verifyProof` (any of the 6 unsigned types) | ~2.43M | +| `submitCompliance` (any of the 6 unsigned types) | ~2.83-2.90M | +| `verifyProofBatch` / `submitComplianceBatch`, 1 entry | ~2.88M | +| ... 2 entries | ~4.84M | +| ... 5 entries | ~12.05M | +| ... 10 entries (max batch) | ~24.08M | + +Signed-variant gas (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) is dominated by in-circuit ECDSA-secp256k1 verification, which roughly doubles proving time off-chain but only modestly increases the verifier byte size; on-chain `verifyProof` for the signed variants is in the same order of magnitude as the unsigned variants. Implementations that target L2s (typical block target 30M-60M gas) MAY raise `MAX_BATCH_SIZE` proportionally. Implementations on chains with lower block-gas budgets MUST lower it. + +Submission overhead beyond verification (~400-470k gas per attestation) covers public-input validation, registry lookups, replay-guard SSTORE, attestation storage, and the `ComplianceVerified` event. Integrators submitting many attestations per user can amortize the per-entry fixed cost via `submitComplianceBatch`. + +**Registry idempotency.** Registry operations (registering merkle roots, reporting thresholds) SHOULD be idempotent-safe: re-registering an already-registered value SHOULD revert to prevent accidental double-registration. Similarly, revoking a value that is not registered SHOULD revert. + +**Emergency circuit break.** Implementations SHOULD include a pause mechanism that can halt proof submissions (and optionally, verifications) in case of a discovered vulnerability in a ZK circuit or verifier contract. Pausing MUST NOT prevent read access to existing attestations, as these are needed for retroactive verification (proof-of-innocence). Implementations SHOULD support per-proof-type pause for surgical incident response without halting unrelated proof types. + +**Trust model and signal honesty.** See [What this standard does NOT prove](#what-this-standard-does-not-prove) in Rationale for the full discussion. In short: the unsigned variants (COMPLIANCE, RISK_SCORE) are self-attested — the circuit verifies the score formula but not the screening signals themselves. Strict-mode jurisdictions MUST reject the unsigned variants; integrators in permissive jurisdictions that require signal honesty MUST require COMPLIANCE_SIGNED / RISK_SCORE_SIGNED, optionally composed with ATTESTATION proofs. + +**ATTESTATION authority root.** ATTESTATION proofs verify Merkle inclusion of a credential leaf in a per-provider credentials tree. The leaf is `H(DOMAIN_CREDENTIAL, provider_id, submitter, type, attribute, expiry)`, which binds the credential to the submitter cryptographically; a forged credential leaf cannot be constructed without breaking Pedersen preimage resistance. However, the circuit does NOT verify a provider signature over the credential leaf or root in-circuit. Authority resolves to the registered publisher EOA: the Oracle records each `credentialRoot` against `(providerId, publisherEOA)`, with a 48 h root TTL and an owner-rotatable publisher. A compromised publisher EOA can publish a tree containing arbitrary `(submitter, attribute)` pairs until the owner rotates the publisher (`setProviderPublisher`, 6 h timelock) or revokes the root (`revokeCredentialRoot`, instant). Implementations whose threat model includes a compromised publisher key SHOULD layer an in-circuit signature scheme over the credential root or credential leaf; this is tracked as future work and is intentionally NOT required by this specification. + +**Cross-chain replay.** The unsigned proof types (COMPLIANCE, RISK_SCORE, PATTERN, ATTESTATION, MEMBERSHIP, NON_MEMBERSHIP) do NOT include a chain identifier as a circuit public input. The same proof bytes may be replayed against the same Oracle on a different chain (or against an alternate Oracle deployment on the same chain), producing independent attestations on each. The on-chain `proofHash = keccak256(proof, proofType, block.chainid, address(this))` and the `_usedProofs[proofHash]` guard prevent replay-into-storage _within_ a given (chain, Oracle) pair, but provide no in-circuit binding. Implementations whose threat model includes cross-deployment replay of unsigned proofs MUST add `chainId` and the verifying contract address as circuit public inputs. + +The signed variants (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) close this gap mathematically: their in-circuit Pedersen digest commits to (`chain_id`, `oracle_address`, `provider_set_hash`, `signals`, `weights`, `timestamp`, `submitter`), and the secp256k1 ECDSA verification of the provider's signature happens over that digest. Replaying a signed proof against a different chain or Oracle requires forging a new ECDSA signature over the new (chain_id, oracle_address) pair under the registered provider's key. Implementations MUST validate `chain_id == block.chainid` and `oracle_address == address(this)` on every signed-variant submission. + +**Verifier-layer reentrancy.** The `IUltraVerifier.verify(...)` interface MUST be declared `view` so that the EVM uses STATICCALL when invoking the verifier. STATICCALL prevents a malicious or compromised verifier from mutating state in the calling contract via reentrant calls. Implementations MUST NOT call verifiers via interfaces that omit the `view` modifier. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From 3b8a04e1b68845f21c655dd1343ac0358e3d6532 Mon Sep 17 00:00:00 2001 From: DROOdotFOO Date: Sat, 16 May 2026 12:23:30 +0200 Subject: [PATCH 02/14] Claim ERC-8338, rename file --- ERCS/{erc-draft_xochi-zkp.md => erc-8338.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ERCS/{erc-draft_xochi-zkp.md => erc-8338.md} (100%) diff --git a/ERCS/erc-draft_xochi-zkp.md b/ERCS/erc-8338.md similarity index 100% rename from ERCS/erc-draft_xochi-zkp.md rename to ERCS/erc-8338.md From 5cad2e3f8b9de3a549fb0a7d4ef23e126e8381c0 Mon Sep 17 00:00:00 2001 From: DROOdotFOO Date: Sat, 16 May 2026 12:26:56 +0200 Subject: [PATCH 03/14] Set eip: 8338 --- ERCS/erc-8338.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERCS/erc-8338.md b/ERCS/erc-8338.md index 608f563f15d..0584a350851 100644 --- a/ERCS/erc-8338.md +++ b/ERCS/erc-8338.md @@ -1,5 +1,5 @@ --- -eip: TBD +eip: 8338 title: Zero-Knowledge Compliance Oracle description: On-chain ZK compliance verification without revealing transaction data author: DROO (@DROOdotFOO), Bloo (@bloo-berries), Merkle Bonsai (@Jabher) From d657e2cba5b2ec8fef71c4d26e1d59293dedbb81 Mon Sep 17 00:00:00 2001 From: DROOdotFOO Date: Sat, 16 May 2026 13:44:45 +0200 Subject: [PATCH 04/14] Drop links to unmerged EIP/ERC drafts in Related Work --- ERCS/erc-8338.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ERCS/erc-8338.md b/ERCS/erc-8338.md index 0584a350851..4a09a52a9c6 100644 --- a/ERCS/erc-8338.md +++ b/ERCS/erc-8338.md @@ -459,17 +459,17 @@ Several existing and emerging standards address compliance, privacy, or on-chain **Privacy Pools (0xbow).** Live on Ethereum mainnet since March 2025. Users prove their withdrawal originates from a "clean" deposit set using ZK proofs, with Association Set Providers (ASPs) maintaining approved deposit lists. The Privacy Pools protocol validates the "prove compliance without revealing data" model. However, set membership is a subset of what regulatory compliance requires. This ERC extends the approach to multi-dimensional compliance: risk scoring, anti-structuring detection, credential verification, and membership/non-membership proofs. -**[EIP-7963](./eip-7963.md).** An oracle-permissioned [ERC-20](./eip-20.md) that validates token transfers via ZK proofs against off-chain payment instructions (ISO 20022 format), using RISC Zero as the proof system. EIP-7963 gates a single token's transfers through a single oracle with a single proof type. This ERC provides standalone compliance attestations with nine proof types, usable by any contract, and is not gated to token operations. +**RISC Zero token oracle.** A draft EIP proposes an oracle-permissioned [ERC-20](./eip-20.md) that validates token transfers via ZK proofs against off-chain payment instructions (ISO 20022 format), using RISC Zero as the proof system. That proposal gates a single token's transfers through a single oracle with a single proof type. This ERC provides standalone compliance attestations with nine proof types, usable by any contract, and is not gated to token operations. **VOSA-RWA.** A compliance-gated privacy token for real-world assets (Draft, 2026). Every token operation requires dual ZK proofs: a compliance attestation (Groth16/BN254, Poseidon hashing) and a transaction conservation proof. VOSA-RWA and this ERC share the "ZK proof for compliance, no PII on-chain" design, but VOSA-RWA embeds compliance into a specific token standard. This ERC is a standalone oracle whose attestations are reusable across protocols. **[ERC-7812](./eip-7812.md).** A ZK identity registry using a singleton Sparse Merkle Tree (80-level, Poseidon on BN128) with custom registrars for business logic. Deployed on Ethereum mainnet. ERC-7812 provides a general-purpose private statement registry. This ERC could operate as a compliance-specific registrar within ERC-7812, storing compliance commitments in its Merkle tree. -**[ERC-8039](./eip-8039.md).** A proof-system-agnostic ZK verification interface for smart accounts (`verifyProof(bytes,bytes) returns (bytes4)`). ERC-8039 standardizes per-relation verifier contracts with a non-reverting return pattern (following [ERC-1271](./eip-1271.md)). This ERC's per-proof-type verifier routing serves a similar verification role but with domain-specific semantics (proof type routing, batch verification, version history). Each generated UltraHonk verifier in this ERC could be wrapped behind an ERC-8039 adapter for smart account integration. +**Smart-account ZK verifier interface.** A draft ERC proposes a proof-system-agnostic ZK verification interface for smart accounts (`verifyProof(bytes,bytes) returns (bytes4)`), standardizing per-relation verifier contracts with a non-reverting return pattern (following [ERC-1271](./eip-1271.md)). This ERC's per-proof-type verifier routing serves a similar verification role but with domain-specific semantics (proof type routing, batch verification, version history). Each generated UltraHonk verifier in this ERC could be wrapped behind such an adapter for smart account integration. **[EIP-7702](./eip-7702.md).** Account abstraction via temporary delegation: an EOA can authorize a contract to execute code on its behalf for a single transaction. EIP-7702 interacts with the `submitter == msg.sender` rule in two ways. First, when a 7702-delegated EOA calls `submitCompliance`, `msg.sender` is the EOA address (not the delegated contract), so the attestation correctly binds to the EOA and the `submitter` public input must equal that EOA. Second, a smart-account batcher (using 7702 to wrap multiple operations) can call `submitComplianceBatch` provided every entry's `submitter` public input equals the delegating EOA. Account-abstraction wallets MUST surface the bound `submitter` address to the user before submission, since a malicious dApp could otherwise solicit proofs bound to the wrong address. The same considerations apply to [ERC-4337](./eip-4337.md) paymasters and ERC-1271 contract signers when used as compliance subjects. -**[ERC-8035](./eip-8035.md) / [ERC-8036](./eip-8036.md) (MultiTrust Credential).** Non-transferable credential anchors with ZK presentation via fixed Groth16 ABI, supporting predicate proofs ("score >= threshold") without revealing raw data. The predicate-proving pattern parallels this ERC's RISK_SCORE proof type. MultiTrust focuses on credential issuance and presentation; this ERC focuses on compliance attestation and retroactive verification. +**MultiTrust Credential.** Companion draft ERCs propose non-transferable credential anchors with ZK presentation via fixed Groth16 ABI, supporting predicate proofs ("score >= threshold") without revealing raw data. The predicate-proving pattern parallels this ERC's RISK_SCORE proof type. MultiTrust focuses on credential issuance and presentation; this ERC focuses on compliance attestation and retroactive verification. **[ERC-1922](./eip-1922.md).** The original zk-SNARK verifier standard (2019, stagnant). ERC-1922 defines a generic interface for on-chain ZK verification with dynamic arrays for cross-scheme compatibility. This ERC supersedes ERC-1922's approach with per-proof-type routing, UltraHonk support, and domain-specific input validation. From a4acfdf23098475ab7b3ac94f428538bab277a77 Mon Sep 17 00:00:00 2001 From: DROOdotFOO Date: Mon, 18 May 2026 16:02:08 +0200 Subject: [PATCH 05/14] ERC-8262: renumber and drop project-name identifiers --- ERCS/{erc-8338.md => erc-8262.md} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename ERCS/{erc-8338.md => erc-8262.md} (99%) diff --git a/ERCS/erc-8338.md b/ERCS/erc-8262.md similarity index 99% rename from ERCS/erc-8338.md rename to ERCS/erc-8262.md index 4a09a52a9c6..f708ca0a82d 100644 --- a/ERCS/erc-8338.md +++ b/ERCS/erc-8262.md @@ -1,5 +1,5 @@ --- -eip: 8338 +eip: 8262 title: Zero-Knowledge Compliance Oracle description: On-chain ZK compliance verification without revealing transaction data author: DROO (@DROOdotFOO), Bloo (@bloo-berries), Merkle Bonsai (@Jabher) @@ -75,7 +75,7 @@ Notes on the proof type semantics: The verifier routes proof verification to per-proof-type verification contracts. Each circuit produces a separate verifier via the ZK backend (e.g., `bb write_solidity_verifier` for Barretenberg's UltraHonk). ```solidity -interface IXochiZKPVerifier { +interface IERC8262Verifier { /// @notice Verify a zero-knowledge compliance proof /// @param proofType The type of proof (0x01-0x09) /// @param proof The encoded proof data @@ -132,12 +132,12 @@ interface IXochiZKPVerifier { } ``` -Implementations MUST also implement [ERC-165](./eip-165.md). `supportsInterface(bytes4)` MUST return `true` for `type(IXochiZKPVerifier).interfaceId` and for `type(IERC165).interfaceId`, and `false` for `0xffffffff`. +Implementations MUST also implement [ERC-165](./eip-165.md). `supportsInterface(bytes4)` MUST return `true` for `type(IERC8262Verifier).interfaceId` and for `type(IERC165).interfaceId`, and `false` for `0xffffffff`. ### Oracle Interface ```solidity -interface IXochiZKPOracle { +interface IERC8262Oracle { struct ComplianceAttestation { address subject; // address that proved compliance (msg.sender at submission) uint8 jurisdictionId; // jurisdiction (0=EU, 1=US, 2=UK, 3=SG) @@ -266,7 +266,7 @@ interface IXochiZKPOracle { } ``` -Implementations MUST also implement [ERC-165](./eip-165.md). `supportsInterface(bytes4)` MUST return `true` for `type(IXochiZKPOracle).interfaceId` and for `type(IERC165).interfaceId`, and `false` for `0xffffffff`. +Implementations MUST also implement [ERC-165](./eip-165.md). `supportsInterface(bytes4)` MUST return `true` for `type(IERC8262Oracle).interfaceId` and for `type(IERC165).interfaceId`, and `false` for `0xffffffff`. ### Jurisdiction Configuration @@ -607,7 +607,7 @@ Signed-variant witnesses (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) are identical to A reference implementation accompanies this ERC. It consists of: -- **Solidity contracts**: `src/XochiZKPVerifier.sol`, `src/XochiZKPOracle.sol`, `src/SettlementRegistry.sol`, `src/XochiTimelock.sol` (Foundry, Solidity 0.8.28, Cancun EVM) +- **Solidity contracts**: `src/ZKComplianceVerifier.sol`, `src/ZKComplianceOracle.sol`, `src/SettlementRegistry.sol`, `src/ComplianceTimelock.sol` (Foundry, Solidity 0.8.28, Cancun EVM) - **Noir circuits**: `circuits/` (one per proof type, pinned to nargo 1.0.0-beta.20 via `.tool-versions`) - **Generated verifiers**: `src/generated/` (UltraHonk verifiers generated by Barretenberg, pinned to bb 4.0.0-nightly.20260120) - **Test suite**: Solidity tests (unit, fuzz, invariant, integration with real proofs for the 6 unsigned proof types) and circuit tests across all 9 circuits. The signed variants (0x07, 0x08, 0x09) are exercised in the TypeScript SDK consumer tests (`test/sdk/`) which generate a fresh ECDSA witness per run. From ae53b058e40f07607f64ca43580cafd4e67f98a4 Mon Sep 17 00:00:00 2001 From: DROOdotFOO Date: Mon, 18 May 2026 17:16:46 +0200 Subject: [PATCH 06/14] ERC-8262: align interface names with IERC8262 convention --- ERCS/erc-8262.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ERCS/erc-8262.md b/ERCS/erc-8262.md index f708ca0a82d..a193f891258 100644 --- a/ERCS/erc-8262.md +++ b/ERCS/erc-8262.md @@ -44,16 +44,16 @@ Implementations MUST support the following proof types. Each type corresponds to All proof types include `submitter` as a public input; implementations MUST enforce `submitter == msg.sender` at submission time. -| Type ID | Name | Circuit | Public inputs | Private inputs | -| ------- | ----------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | -| 0x01 | Compliance | compliance | jurisdiction_id, provider_set_hash, config_hash, timestamp, meets_threshold, submitter | signals, weights, weight_sum, provider_ids, num_providers | -| 0x02 | Risk Score | risk_score | proof_type (threshold/range), direction, bound_lower, bound_upper, result, config_hash, provider_set_hash, submitter | signals, weights, weight_sum, provider_ids, num_providers | -| 0x03 | Pattern | pattern | analysis_type, result, reporting_threshold, time_window, tx_set_hash, submitter, settlement_root | amounts, timestamps, num_transactions | -| 0x04 | Attestation | attestation | provider_id, credential_type, is_valid, credential_root, current_timestamp, submitter | credential_attribute, expiry_timestamp, merkle_index, merkle_path | -| 0x05 | Membership | membership | merkle_root, set_id, timestamp, is_member, submitter | subject_salt, merkle_index, merkle_path | -| 0x06 | Non-membership | non_membership | merkle_root, set_id, timestamp, is_non_member, submitter | low_leaf, low_leaf_salt, low_index, low_path, high_leaf, high_leaf_salt, high_index, high_path | -| 0x07 | Compliance Signed | compliance_signed | jurisdiction_id, provider_set_hash, config_hash, timestamp, meets_threshold, signer_pubkey_hash, chain_id, oracle_address, submitter | signals, weights, weight_sum, provider_ids, num_providers, signature, pubkey_x, pubkey_y | -| 0x08 | Risk Score Signed | risk_score_signed | proof_type, direction, bound_lower, bound_upper, result, config_hash, provider_set_hash, signer_pubkey_hash, chain_id, oracle_address, submitter | signals, weights, weight_sum, provider_ids, num_providers, signature, pubkey_x, pubkey_y, signed_timestamp | +| Type ID | Name | Circuit | Public inputs | Private inputs | +| ------- | ----------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | +| 0x01 | Compliance | compliance | jurisdiction_id, provider_set_hash, config_hash, timestamp, meets_threshold, submitter | signals, weights, weight_sum, provider_ids, num_providers | +| 0x02 | Risk Score | risk_score | proof_type (threshold/range), direction, bound_lower, bound_upper, result, config_hash, provider_set_hash, submitter | signals, weights, weight_sum, provider_ids, num_providers | +| 0x03 | Pattern | pattern | analysis_type, result, reporting_threshold, time_window, tx_set_hash, submitter, settlement_root | amounts, timestamps, num_transactions | +| 0x04 | Attestation | attestation | provider_id, credential_type, is_valid, credential_root, current_timestamp, submitter | credential_attribute, expiry_timestamp, merkle_index, merkle_path | +| 0x05 | Membership | membership | merkle_root, set_id, timestamp, is_member, submitter | subject_salt, merkle_index, merkle_path | +| 0x06 | Non-membership | non_membership | merkle_root, set_id, timestamp, is_non_member, submitter | low_leaf, low_leaf_salt, low_index, low_path, high_leaf, high_leaf_salt, high_index, high_path | +| 0x07 | Compliance Signed | compliance_signed | jurisdiction_id, provider_set_hash, config_hash, timestamp, meets_threshold, signer_pubkey_hash, chain_id, oracle_address, submitter | signals, weights, weight_sum, provider_ids, num_providers, signature, pubkey_x, pubkey_y | +| 0x08 | Risk Score Signed | risk_score_signed | proof_type, direction, bound_lower, bound_upper, result, config_hash, provider_set_hash, signer_pubkey_hash, chain_id, oracle_address, submitter | signals, weights, weight_sum, provider_ids, num_providers, signature, pubkey_x, pubkey_y, signed_timestamp | | 0x09 | Compliance Multi-Signed | compliance_multi_signed | jurisdiction_id, provider_set_hash, config_hash, timestamp, meets_threshold, threshold_m, signer_pubkey_hash_0..4, chain_id, oracle_address, submitter | per-slot signals/weights/weight_sums/pubkey_x/pubkey_y/signature (5 slots each) | Notes on the proof type semantics: @@ -607,7 +607,7 @@ Signed-variant witnesses (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) are identical to A reference implementation accompanies this ERC. It consists of: -- **Solidity contracts**: `src/ZKComplianceVerifier.sol`, `src/ZKComplianceOracle.sol`, `src/SettlementRegistry.sol`, `src/ComplianceTimelock.sol` (Foundry, Solidity 0.8.28, Cancun EVM) +- **Solidity contracts**: `src/ERC8262Verifier.sol`, `src/ERC8262Oracle.sol`, `src/SettlementRegistry.sol`, `src/Timelock.sol` (Foundry, Solidity 0.8.28, Cancun EVM) - **Noir circuits**: `circuits/` (one per proof type, pinned to nargo 1.0.0-beta.20 via `.tool-versions`) - **Generated verifiers**: `src/generated/` (UltraHonk verifiers generated by Barretenberg, pinned to bb 4.0.0-nightly.20260120) - **Test suite**: Solidity tests (unit, fuzz, invariant, integration with real proofs for the 6 unsigned proof types) and circuit tests across all 9 circuits. The signed variants (0x07, 0x08, 0x09) are exercised in the TypeScript SDK consumer tests (`test/sdk/`) which generate a fresh ECDSA witness per run. From 422d73b8f9801f6317b2a627ef2c54e31a109920 Mon Sep 17 00:00:00 2001 From: DROOdotFOO Date: Mon, 18 May 2026 19:22:39 +0200 Subject: [PATCH 07/14] ERC-8262: link impl refs to xochi-fi, fix iff --- ERCS/erc-8262.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ERCS/erc-8262.md b/ERCS/erc-8262.md index a193f891258..b42b8254da7 100644 --- a/ERCS/erc-8262.md +++ b/ERCS/erc-8262.md @@ -68,7 +68,7 @@ Notes on the proof type semantics: - **Provider-signed variants (0x07 Compliance Signed, 0x08 Risk Score Signed).** Identical semantics to their unsigned siblings, plus an in-circuit secp256k1 ECDSA verification of a Pedersen digest committing to `(chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`. The provider's pubkey commitment is exposed as `signer_pubkey_hash`; implementations MUST validate it against an on-chain registry. The `chain_id` and `oracle_address` public inputs MUST match `block.chainid` and the consuming Oracle's address: this binds a single provider signature to one deployment so the same signed payload cannot mint attestations across chains or against alternate Oracle deployments. Strict-mode jurisdictions (e.g. US BSA, Singapore) reject the unsigned siblings entirely; permissive jurisdictions accept either form. -- **Compliance Multi-Signed (0x09).** Extends the signed model to M-of-N. The circuit bundles up to five parallel signer slots; a slot is active iff its public `signer_pubkey_hash` is non-zero. Each active slot independently verifies a secp256k1 signature over a slot-specific Pedersen digest carrying its own `slot_index` (under a distinct `DOMAIN_MULTI_SIGNED_SIGNALS` tag) and independently asserts the per-provider risk score is below the jurisdiction high-risk floor. The Oracle MUST validate each non-zero slot's `signer_pubkey_hash` against the registry, MUST reject duplicate hashes across active slots, MUST enforce `chain_id == block.chainid` and `oracle_address == address(this)`, and MUST enforce `threshold_m >= JurisdictionConfig.minMultiProviderThreshold(jurisdictionId)` (e.g., US BSA and Singapore require M >= 2; permissive jurisdictions accept M >= 1). Forging an attestation under 0x09 requires compromising at least M of the N registered signing keys simultaneously. +- **Compliance Multi-Signed (0x09).** Extends the signed model to M-of-N. The circuit bundles up to five parallel signer slots; a slot is active if its public `signer_pubkey_hash` is non-zero. Each active slot independently verifies a secp256k1 signature over a slot-specific Pedersen digest carrying its own `slot_index` (under a distinct `DOMAIN_MULTI_SIGNED_SIGNALS` tag) and independently asserts the per-provider risk score is below the jurisdiction high-risk floor. The Oracle MUST validate each non-zero slot's `signer_pubkey_hash` against the registry, MUST reject duplicate hashes across active slots, MUST enforce `chain_id == block.chainid` and `oracle_address == address(this)`, and MUST enforce `threshold_m >= JurisdictionConfig.minMultiProviderThreshold(jurisdictionId)` (e.g., US BSA and Singapore require M >= 2; permissive jurisdictions accept M >= 1). Forging an attestation under 0x09 requires compromising at least M of the N registered signing keys simultaneously. ### Verifier Interface @@ -479,7 +479,7 @@ This ERC introduces new interfaces and does not modify existing standards. It is ## Test Cases -The reference implementation includes binary proof fixtures in `test/fixtures/` for the six unsigned proof types. Static fixtures are not provided for the three signed variants (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED, COMPLIANCE_MULTI_SIGNED) because each requires a fresh secp256k1 ECDSA witness; those are exercised end-to-end in the TypeScript SDK consumer tests instead. Each unsigned fixture contains: +The reference implementation includes binary proof fixtures in [`test/fixtures/`](https://github.com/xochi-fi/erc-8262/tree/main/test/fixtures) for the six unsigned proof types. Static fixtures are not provided for the three signed variants (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED, COMPLIANCE_MULTI_SIGNED) because each requires a fresh secp256k1 ECDSA witness; those are exercised end-to-end in the TypeScript SDK consumer tests instead. Each unsigned fixture contains: - `proof`: the raw UltraHonk proof bytes (8640 bytes each) - `public_inputs`: the packed bytes32 public inputs @@ -493,7 +493,7 @@ The reference implementation includes binary proof fixtures in `test/fixtures/` | MEMBERSHIP | 160 bytes (5 inputs) | merkle_root, set_id, timestamp, is_member, submitter | | NON_MEMBERSHIP | 160 bytes (5 inputs) | merkle_root, set_id, timestamp, is_non_member, submitter | -All fixtures use Pedersen hash (Noir stdlib) for in-circuit commitments and Merkle tree construction. Fixtures can be regenerated via `scripts/generate-fixtures.sh`. +All fixtures use Pedersen hash (Noir stdlib) for in-circuit commitments and Merkle tree construction. Fixtures can be regenerated via [`scripts/generate-fixtures.sh`](https://github.com/xochi-fi/erc-8262/blob/main/scripts/generate-fixtures.sh). ### Witness Annex @@ -601,18 +601,18 @@ submitter = "0xdead" # AND high_index == low_index + 1. ``` -Signed-variant witnesses (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) are identical to their unsigned siblings in the screening payload, plus `pubkey_x`, `pubkey_y`, `signature`, `signer_pubkey_hash`, `chain_id`, and `oracle_address`. The signature is computed off-chain by the provider over `H_pedersen(chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`. COMPLIANCE_MULTI_SIGNED (0x09) extends this to five parallel signer slots: each active slot supplies its own `(signals, weights, weight_sum, pubkey_x, pubkey_y, signature)` and a non-zero `signer_pubkey_hash`, where each signature commits to a slot-specific Pedersen digest under the `DOMAIN_MULTI_SIGNED_SIGNALS` tag (with embedded `slot_index`). Implementations producing fresh fixtures MUST sample fresh nonces — the reference implementation does this in `test/sdk/` rather than committing a static witness. +Signed-variant witnesses (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) are identical to their unsigned siblings in the screening payload, plus `pubkey_x`, `pubkey_y`, `signature`, `signer_pubkey_hash`, `chain_id`, and `oracle_address`. The signature is computed off-chain by the provider over `H_pedersen(chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`. COMPLIANCE_MULTI_SIGNED (0x09) extends this to five parallel signer slots: each active slot supplies its own `(signals, weights, weight_sum, pubkey_x, pubkey_y, signature)` and a non-zero `signer_pubkey_hash`, where each signature commits to a slot-specific Pedersen digest under the `DOMAIN_MULTI_SIGNED_SIGNALS` tag (with embedded `slot_index`). Implementations producing fresh fixtures MUST sample fresh nonces — the reference implementation does this in [`test/sdk/`](https://github.com/xochi-fi/erc-8262/tree/main/test/sdk) rather than committing a static witness. ## Reference Implementation A reference implementation accompanies this ERC. It consists of: -- **Solidity contracts**: `src/ERC8262Verifier.sol`, `src/ERC8262Oracle.sol`, `src/SettlementRegistry.sol`, `src/Timelock.sol` (Foundry, Solidity 0.8.28, Cancun EVM) -- **Noir circuits**: `circuits/` (one per proof type, pinned to nargo 1.0.0-beta.20 via `.tool-versions`) -- **Generated verifiers**: `src/generated/` (UltraHonk verifiers generated by Barretenberg, pinned to bb 4.0.0-nightly.20260120) -- **Test suite**: Solidity tests (unit, fuzz, invariant, integration with real proofs for the 6 unsigned proof types) and circuit tests across all 9 circuits. The signed variants (0x07, 0x08, 0x09) are exercised in the TypeScript SDK consumer tests (`test/sdk/`) which generate a fresh ECDSA witness per run. +- **Solidity contracts**: [`src/ERC8262Verifier.sol`](https://github.com/xochi-fi/erc-8262/blob/main/src/ERC8262Verifier.sol), [`src/ERC8262Oracle.sol`](https://github.com/xochi-fi/erc-8262/blob/main/src/ERC8262Oracle.sol), [`src/SettlementRegistry.sol`](https://github.com/xochi-fi/erc-8262/blob/main/src/SettlementRegistry.sol), [`src/Timelock.sol`](https://github.com/xochi-fi/erc-8262/blob/main/src/Timelock.sol) (Foundry, Solidity 0.8.28, Cancun EVM) +- **Noir circuits**: [`circuits/`](https://github.com/xochi-fi/erc-8262/tree/main/circuits) (one per proof type, pinned to nargo 1.0.0-beta.20 via [`.tool-versions`](https://github.com/xochi-fi/erc-8262/blob/main/.tool-versions)) +- **Generated verifiers**: [`src/generated/`](https://github.com/xochi-fi/erc-8262/tree/main/src/generated) (UltraHonk verifiers generated by Barretenberg, pinned to bb 4.0.0-nightly.20260120) +- **Test suite**: Solidity tests (unit, fuzz, invariant, integration with real proofs for the 6 unsigned proof types) and circuit tests across all 9 circuits. The signed variants (0x07, 0x08, 0x09) are exercised in the TypeScript SDK consumer tests ([`test/sdk/`](https://github.com/xochi-fi/erc-8262/tree/main/test/sdk)) which generate a fresh ECDSA witness per run. -Thanks to Merkle Bonsai (@Jabher) for reviewing the generated UltraHonk verifiers and identifying that the `pairing()` free function could be rewritten in inline Yul to bring all nine per-proof-type verifiers under the [EIP-170](./eip-170.md) 24,576-byte runtime size limit. The reference implementation incorporates the rewrite in `scripts/patch-pairing-yul.sh`, saving ~186 bytes per verifier (and ~800 gas per `verifyProof` call as a bonus) while staying byte-identical to the `bb`-generated semantics on the pairing precompile (`address(0x08)`) input layout. +Thanks to Merkle Bonsai (@Jabher) for reviewing the generated UltraHonk verifiers and identifying that the `pairing()` free function could be rewritten in inline Yul to bring all nine per-proof-type verifiers under the [EIP-170](./eip-170.md) 24,576-byte runtime size limit. The reference implementation incorporates the rewrite in [`scripts/patch-pairing-yul.sh`](https://github.com/xochi-fi/erc-8262/blob/main/scripts/patch-pairing-yul.sh), saving ~186 bytes per verifier (and ~800 gas per `verifyProof` call as a bonus) while staying byte-identical to the `bb`-generated semantics on the pairing precompile (`address(0x08)`) input layout. ## Security Considerations @@ -626,7 +626,7 @@ Thanks to Merkle Bonsai (@Jabher) for reviewing the generated UltraHonk verifier **Front-running the oracle.** Compliance proofs are generated before settlement. An adversary who observes a proof submission could infer a trade is about to occur. Implementations SHOULD batch proof submissions or submit them as part of the settlement transaction to minimize information leakage. -**Administrative operations.** Verifier contract updates and provider weight changes are privileged operations. Implementations SHOULD use a two-step ownership transfer pattern (transferOwnership + acceptOwnership) to prevent accidental transfer to an incorrect address. Critical operations (verifier replacement, TTL changes) SHOULD be timelocked in production deployments. Implementations SHOULD further split administrative authority into role classes with bounded blast radius -- for example, a pause-only "guardian" role distinct from registry-mutating and config-mutating roles -- so that a single compromised key cannot both pause and rewrite registries. The reference implementation uses a three-role split (GUARDIAN, REGISTRAR, CONFIG) under a 2-tier selector-gated timelock; see `docs/THREAT_MODEL.md` for the full per-role capability matrix and the timelock-tier mapping. +**Administrative operations.** Verifier contract updates and provider weight changes are privileged operations. Implementations SHOULD use a two-step ownership transfer pattern (transferOwnership + acceptOwnership) to prevent accidental transfer to an incorrect address. Critical operations (verifier replacement, TTL changes) SHOULD be timelocked in production deployments. Implementations SHOULD further split administrative authority into role classes with bounded blast radius -- for example, a pause-only "guardian" role distinct from registry-mutating and config-mutating roles -- so that a single compromised key cannot both pause and rewrite registries. The reference implementation uses a three-role split (GUARDIAN, REGISTRAR, CONFIG) under a 2-tier selector-gated timelock; see [`docs/THREAT_MODEL.md`](https://github.com/xochi-fi/erc-8262/blob/main/docs/THREAT_MODEL.md) for the full per-role capability matrix and the timelock-tier mapping. **Public input validation.** Implementations MUST validate public inputs for every proof type, not just the primary compliance proof. Without validation, a prover can generate a proof for one context (e.g., a lenient jurisdiction's reporting threshold) and submit it for a different context. Specifically: @@ -651,7 +651,7 @@ Thanks to Merkle Bonsai (@Jabher) for reviewing the generated UltraHonk verifier **Batch verification limits.** Implementations MUST enforce a maximum batch size for `verifyProofBatch()` to prevent unbounded gas consumption. The reference implementation uses a limit of 10 proofs per batch, sized to fit comfortably under a 30M-gas mainnet block ceiling (approximately 24M gas at the maximum batch size, with ~5M gas of headroom). Implementations targeting chains with different block-gas budgets SHOULD recalibrate the limit accordingly. -**Expected gas costs.** UltraHonk verification dominates total transaction cost. The following figures are measured against the reference implementation on the Cancun EVM with real proofs (see `test/GasBenchmark.t.sol`); other proof systems and circuit revisions will differ. +**Expected gas costs.** UltraHonk verification dominates total transaction cost. The following figures are measured against the reference implementation on the Cancun EVM with real proofs (see [`test/GasBenchmark.t.sol`](https://github.com/xochi-fi/erc-8262/blob/main/test/GasBenchmark.t.sol)); other proof systems and circuit revisions will differ. | Operation | Approx. gas | | ----------------------------------------------------- | ----------- | @@ -682,4 +682,4 @@ The signed variants (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) close this gap mathem ## Copyright -Copyright and related rights waived via [CC0](../LICENSE.md). +Copyright and related rights waived via [CC0](https://github.com/ethereum/EIPs/blob/master/LICENSE). From d97fb5597fd38736db60991b61a2b93e1bf1c505 Mon Sep 17 00:00:00 2001 From: DROOdotFOO Date: Mon, 18 May 2026 20:29:21 +0200 Subject: [PATCH 08/14] ERC-8262: update discussions-to slug to include number --- ERCS/erc-8262.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERCS/erc-8262.md b/ERCS/erc-8262.md index b42b8254da7..1f091954640 100644 --- a/ERCS/erc-8262.md +++ b/ERCS/erc-8262.md @@ -3,7 +3,7 @@ eip: 8262 title: Zero-Knowledge Compliance Oracle description: On-chain ZK compliance verification without revealing transaction data author: DROO (@DROOdotFOO), Bloo (@bloo-berries), Merkle Bonsai (@Jabher) -discussions-to: https://ethereum-magicians.org/t/erc-zero-knowledge-compliance-oracle/28543 +discussions-to: https://ethereum-magicians.org/t/erc-8262-zero-knowledge-compliance-oracle/28543 status: Draft type: Standards Track category: ERC From a23fc9df656ac6debd5195dff715173616748a6c Mon Sep 17 00:00:00 2001 From: DROOdotFOO Date: Mon, 18 May 2026 21:25:27 +0200 Subject: [PATCH 09/14] ERC-8262: drop non-relative links for eipw --- ERCS/erc-8262.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ERCS/erc-8262.md b/ERCS/erc-8262.md index 1f091954640..a1425f2377c 100644 --- a/ERCS/erc-8262.md +++ b/ERCS/erc-8262.md @@ -479,7 +479,7 @@ This ERC introduces new interfaces and does not modify existing standards. It is ## Test Cases -The reference implementation includes binary proof fixtures in [`test/fixtures/`](https://github.com/xochi-fi/erc-8262/tree/main/test/fixtures) for the six unsigned proof types. Static fixtures are not provided for the three signed variants (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED, COMPLIANCE_MULTI_SIGNED) because each requires a fresh secp256k1 ECDSA witness; those are exercised end-to-end in the TypeScript SDK consumer tests instead. Each unsigned fixture contains: +The reference implementation includes binary proof fixtures in `test/fixtures/` for the six unsigned proof types. Static fixtures are not provided for the three signed variants (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED, COMPLIANCE_MULTI_SIGNED) because each requires a fresh secp256k1 ECDSA witness; those are exercised end-to-end in the TypeScript SDK consumer tests instead. Each unsigned fixture contains: - `proof`: the raw UltraHonk proof bytes (8640 bytes each) - `public_inputs`: the packed bytes32 public inputs @@ -493,7 +493,7 @@ The reference implementation includes binary proof fixtures in [`test/fixtures/` | MEMBERSHIP | 160 bytes (5 inputs) | merkle_root, set_id, timestamp, is_member, submitter | | NON_MEMBERSHIP | 160 bytes (5 inputs) | merkle_root, set_id, timestamp, is_non_member, submitter | -All fixtures use Pedersen hash (Noir stdlib) for in-circuit commitments and Merkle tree construction. Fixtures can be regenerated via [`scripts/generate-fixtures.sh`](https://github.com/xochi-fi/erc-8262/blob/main/scripts/generate-fixtures.sh). +All fixtures use Pedersen hash (Noir stdlib) for in-circuit commitments and Merkle tree construction. Fixtures can be regenerated via `scripts/generate-fixtures.sh`. ### Witness Annex @@ -601,18 +601,18 @@ submitter = "0xdead" # AND high_index == low_index + 1. ``` -Signed-variant witnesses (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) are identical to their unsigned siblings in the screening payload, plus `pubkey_x`, `pubkey_y`, `signature`, `signer_pubkey_hash`, `chain_id`, and `oracle_address`. The signature is computed off-chain by the provider over `H_pedersen(chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`. COMPLIANCE_MULTI_SIGNED (0x09) extends this to five parallel signer slots: each active slot supplies its own `(signals, weights, weight_sum, pubkey_x, pubkey_y, signature)` and a non-zero `signer_pubkey_hash`, where each signature commits to a slot-specific Pedersen digest under the `DOMAIN_MULTI_SIGNED_SIGNALS` tag (with embedded `slot_index`). Implementations producing fresh fixtures MUST sample fresh nonces — the reference implementation does this in [`test/sdk/`](https://github.com/xochi-fi/erc-8262/tree/main/test/sdk) rather than committing a static witness. +Signed-variant witnesses (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) are identical to their unsigned siblings in the screening payload, plus `pubkey_x`, `pubkey_y`, `signature`, `signer_pubkey_hash`, `chain_id`, and `oracle_address`. The signature is computed off-chain by the provider over `H_pedersen(chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`. COMPLIANCE_MULTI_SIGNED (0x09) extends this to five parallel signer slots: each active slot supplies its own `(signals, weights, weight_sum, pubkey_x, pubkey_y, signature)` and a non-zero `signer_pubkey_hash`, where each signature commits to a slot-specific Pedersen digest under the `DOMAIN_MULTI_SIGNED_SIGNALS` tag (with embedded `slot_index`). Implementations producing fresh fixtures MUST sample fresh nonces — the reference implementation does this in `test/sdk/` rather than committing a static witness. ## Reference Implementation A reference implementation accompanies this ERC. It consists of: -- **Solidity contracts**: [`src/ERC8262Verifier.sol`](https://github.com/xochi-fi/erc-8262/blob/main/src/ERC8262Verifier.sol), [`src/ERC8262Oracle.sol`](https://github.com/xochi-fi/erc-8262/blob/main/src/ERC8262Oracle.sol), [`src/SettlementRegistry.sol`](https://github.com/xochi-fi/erc-8262/blob/main/src/SettlementRegistry.sol), [`src/Timelock.sol`](https://github.com/xochi-fi/erc-8262/blob/main/src/Timelock.sol) (Foundry, Solidity 0.8.28, Cancun EVM) -- **Noir circuits**: [`circuits/`](https://github.com/xochi-fi/erc-8262/tree/main/circuits) (one per proof type, pinned to nargo 1.0.0-beta.20 via [`.tool-versions`](https://github.com/xochi-fi/erc-8262/blob/main/.tool-versions)) -- **Generated verifiers**: [`src/generated/`](https://github.com/xochi-fi/erc-8262/tree/main/src/generated) (UltraHonk verifiers generated by Barretenberg, pinned to bb 4.0.0-nightly.20260120) -- **Test suite**: Solidity tests (unit, fuzz, invariant, integration with real proofs for the 6 unsigned proof types) and circuit tests across all 9 circuits. The signed variants (0x07, 0x08, 0x09) are exercised in the TypeScript SDK consumer tests ([`test/sdk/`](https://github.com/xochi-fi/erc-8262/tree/main/test/sdk)) which generate a fresh ECDSA witness per run. +- **Solidity contracts**: `src/ERC8262Verifier.sol`, `src/ERC8262Oracle.sol`, `src/SettlementRegistry.sol`, `src/Timelock.sol` (Foundry, Solidity 0.8.28, Cancun EVM) +- **Noir circuits**: `circuits/` (one per proof type, pinned to nargo 1.0.0-beta.20 via `.tool-versions`) +- **Generated verifiers**: `src/generated/` (UltraHonk verifiers generated by Barretenberg, pinned to bb 4.0.0-nightly.20260120) +- **Test suite**: Solidity tests (unit, fuzz, invariant, integration with real proofs for the 6 unsigned proof types) and circuit tests across all 9 circuits. The signed variants (0x07, 0x08, 0x09) are exercised in the TypeScript SDK consumer tests (`test/sdk/`) which generate a fresh ECDSA witness per run. -Thanks to Merkle Bonsai (@Jabher) for reviewing the generated UltraHonk verifiers and identifying that the `pairing()` free function could be rewritten in inline Yul to bring all nine per-proof-type verifiers under the [EIP-170](./eip-170.md) 24,576-byte runtime size limit. The reference implementation incorporates the rewrite in [`scripts/patch-pairing-yul.sh`](https://github.com/xochi-fi/erc-8262/blob/main/scripts/patch-pairing-yul.sh), saving ~186 bytes per verifier (and ~800 gas per `verifyProof` call as a bonus) while staying byte-identical to the `bb`-generated semantics on the pairing precompile (`address(0x08)`) input layout. +Thanks to Merkle Bonsai (@Jabher) for reviewing the generated UltraHonk verifiers and identifying that the `pairing()` free function could be rewritten in inline Yul to bring all nine per-proof-type verifiers under the [EIP-170](./eip-170.md) 24,576-byte runtime size limit. The reference implementation incorporates the rewrite in `scripts/patch-pairing-yul.sh`, saving ~186 bytes per verifier (and ~800 gas per `verifyProof` call as a bonus) while staying byte-identical to the `bb`-generated semantics on the pairing precompile (`address(0x08)`) input layout. ## Security Considerations @@ -626,7 +626,7 @@ Thanks to Merkle Bonsai (@Jabher) for reviewing the generated UltraHonk verifier **Front-running the oracle.** Compliance proofs are generated before settlement. An adversary who observes a proof submission could infer a trade is about to occur. Implementations SHOULD batch proof submissions or submit them as part of the settlement transaction to minimize information leakage. -**Administrative operations.** Verifier contract updates and provider weight changes are privileged operations. Implementations SHOULD use a two-step ownership transfer pattern (transferOwnership + acceptOwnership) to prevent accidental transfer to an incorrect address. Critical operations (verifier replacement, TTL changes) SHOULD be timelocked in production deployments. Implementations SHOULD further split administrative authority into role classes with bounded blast radius -- for example, a pause-only "guardian" role distinct from registry-mutating and config-mutating roles -- so that a single compromised key cannot both pause and rewrite registries. The reference implementation uses a three-role split (GUARDIAN, REGISTRAR, CONFIG) under a 2-tier selector-gated timelock; see [`docs/THREAT_MODEL.md`](https://github.com/xochi-fi/erc-8262/blob/main/docs/THREAT_MODEL.md) for the full per-role capability matrix and the timelock-tier mapping. +**Administrative operations.** Verifier contract updates and provider weight changes are privileged operations. Implementations SHOULD use a two-step ownership transfer pattern (transferOwnership + acceptOwnership) to prevent accidental transfer to an incorrect address. Critical operations (verifier replacement, TTL changes) SHOULD be timelocked in production deployments. Implementations SHOULD further split administrative authority into role classes with bounded blast radius -- for example, a pause-only "guardian" role distinct from registry-mutating and config-mutating roles -- so that a single compromised key cannot both pause and rewrite registries. The reference implementation uses a three-role split (GUARDIAN, REGISTRAR, CONFIG) under a 2-tier selector-gated timelock; see `docs/THREAT_MODEL.md` in the reference implementation repository for the full per-role capability matrix and the timelock-tier mapping. **Public input validation.** Implementations MUST validate public inputs for every proof type, not just the primary compliance proof. Without validation, a prover can generate a proof for one context (e.g., a lenient jurisdiction's reporting threshold) and submit it for a different context. Specifically: @@ -651,7 +651,7 @@ Thanks to Merkle Bonsai (@Jabher) for reviewing the generated UltraHonk verifier **Batch verification limits.** Implementations MUST enforce a maximum batch size for `verifyProofBatch()` to prevent unbounded gas consumption. The reference implementation uses a limit of 10 proofs per batch, sized to fit comfortably under a 30M-gas mainnet block ceiling (approximately 24M gas at the maximum batch size, with ~5M gas of headroom). Implementations targeting chains with different block-gas budgets SHOULD recalibrate the limit accordingly. -**Expected gas costs.** UltraHonk verification dominates total transaction cost. The following figures are measured against the reference implementation on the Cancun EVM with real proofs (see [`test/GasBenchmark.t.sol`](https://github.com/xochi-fi/erc-8262/blob/main/test/GasBenchmark.t.sol)); other proof systems and circuit revisions will differ. +**Expected gas costs.** UltraHonk verification dominates total transaction cost. The following figures are measured against the reference implementation on the Cancun EVM with real proofs (see `test/GasBenchmark.t.sol` in the reference implementation repository); other proof systems and circuit revisions will differ. | Operation | Approx. gas | | ----------------------------------------------------- | ----------- | @@ -682,4 +682,4 @@ The signed variants (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) close this gap mathem ## Copyright -Copyright and related rights waived via [CC0](https://github.com/ethereum/EIPs/blob/master/LICENSE). +Copyright and related rights waived via [CC0](../LICENSE.md). From ccec34b9d134c2c2524b4b9c3908e34a21440998 Mon Sep 17 00:00:00 2001 From: DROOdotFOO Date: Wed, 3 Jun 2026 16:42:59 +0200 Subject: [PATCH 10/14] Address review feedback from SamWilsn (PR #1747) - Expand AML, TEE on first use; drop product names (Uniswap, CoW, Tornado, SGX) per reviewer suggestion - Move Merkle Bonsai attribution to HTML comment - Restructure Security Considerations as pure threat-model (0 RFC-2119 keywords); each paragraph links to the Spec subsection that mandates the mitigation - Add Per-Type Circuit Specifications (9 H4s, one per proof type), Circuit Conventions, and Commitment Layouts so the ZK constraint logic and field layouts are concrete enough to implement without reading the reference circuits - Migrate scattered Security MUSTs into new Spec subsections: Proof System Requirements, Batch Verification Limits, Proof Hash Computation, Circuit Constraints, Pause Mechanism, Administrative Operations, Trust Tier Disclosure - Extend existing Spec subsections (Verifier Interface view modifier, Verifier Versioning TOCTOU, Provider Weight Publication bounded history, Validation Registries idempotency + credential publisher rotation, Risk Score Computation collusion guidance) - Convert backtick section references to markdown anchors - Softer Rationale wording: replace orphan SHOULD with a cross-reference to Trust Tier Disclosure; reword inline MUSTs that are wallet-author / integrator guidance --- ERCS/erc-8262.md | 306 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 260 insertions(+), 46 deletions(-) diff --git a/ERCS/erc-8262.md b/ERCS/erc-8262.md index a1425f2377c..7af017c70f7 100644 --- a/ERCS/erc-8262.md +++ b/ERCS/erc-8262.md @@ -13,16 +13,16 @@ requires: 165 ## Abstract -A standard interface for on-chain verification of regulatory compliance (AML, sanctions screening, anti-structuring) using zero-knowledge proofs. Users generate proofs client-side that attest to compliance with jurisdiction-specific thresholds without revealing transaction amounts, counterparty identities, or screening details. Verifiers confirm proof validity on-chain. No trusted third party or TEE is required. +A standard interface for on-chain verification of regulatory compliance (anti-money laundering, sanctions screening, anti-structuring) using zero-knowledge proofs. Users generate proofs client-side that attest to compliance with jurisdiction-specific thresholds without revealing transaction amounts, counterparty identities, or screening details. Verifiers confirm proof validity on-chain. No trusted third party or trusted execution environment (TEE) is required. ## Motivation -Public blockchains force a binary choice between transparency and privacy. Transparent execution (Uniswap, CoW Protocol) exposes trades to billions in cumulative MEV extraction. Privacy tools (Tornado Cash) have been sanctioned for lacking compliance mechanisms. +Public blockchains force a binary choice between transparency and privacy. Transparent execution exposes trades to billions in cumulative MEV extraction. Privacy tools have been sanctioned for lacking compliance mechanisms. Existing approaches to compliant privacy fall short: - **View keys** (Railgun, Panther): Trade privately, then reveal raw transaction data to auditors on request. This leaks the data: it is delayed transparency. -- **TEE-based compliance** (various): Rely on hardware trust assumptions that have been broken repeatedly (SGX side channels, key extraction). +- **TEE-based compliance** (various): Rely on hardware trust assumptions that have been broken by side-channel attacks and key extraction. - **Compliance-by-exclusion** (Privacy Pools): Prove you're NOT in a bad set. Doesn't prove you ARE compliant with specific jurisdiction rules. This ERC defines a standard where compliance is proven cryptographically at transaction time. The proof commits to screening results, jurisdiction thresholds, and provider attestations. Regulators verify a proof. They never see the underlying data. @@ -37,6 +37,10 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S - **providerConfigHash**: A hash of the global provider weight configuration published by the oracle administrator. Versioned on-chain; weight changes push a new entry to the config history. - **attestation TTL**: The duration (in seconds) for which a compliance attestation remains valid after on-chain recording. Expired attestations remain queryable via `getHistoricalProof()` but are not considered valid by `checkCompliance()`. +### Proof System Requirements + +Implementations MUST use a ZK proof system that achieves at least 128-bit security against forgery. Groth16, PLONK, and UltraHonk (Noir/Aztec) all meet this bar. The reference implementation uses UltraHonk. + ### Proof Types Implementations MUST support the following proof types. Each type corresponds to a separate ZK circuit with its own verification key. @@ -70,6 +74,191 @@ Notes on the proof type semantics: - **Compliance Multi-Signed (0x09).** Extends the signed model to M-of-N. The circuit bundles up to five parallel signer slots; a slot is active if its public `signer_pubkey_hash` is non-zero. Each active slot independently verifies a secp256k1 signature over a slot-specific Pedersen digest carrying its own `slot_index` (under a distinct `DOMAIN_MULTI_SIGNED_SIGNALS` tag) and independently asserts the per-provider risk score is below the jurisdiction high-risk floor. The Oracle MUST validate each non-zero slot's `signer_pubkey_hash` against the registry, MUST reject duplicate hashes across active slots, MUST enforce `chain_id == block.chainid` and `oracle_address == address(this)`, and MUST enforce `threshold_m >= JurisdictionConfig.minMultiProviderThreshold(jurisdictionId)` (e.g., US BSA and Singapore require M >= 2; permissive jurisdictions accept M >= 1). Forging an attestation under 0x09 requires compromising at least M of the N registered signing keys simultaneously. +### Circuit Conventions + +The following structural constants are normative. Implementations that deviate produce verifiers incompatible with other deployments and cannot share registries. + +| Constant | Value | Applies to | +| --------------------- | ----- | ----------------------------------------------------------- | +| `MAX_PROVIDERS` | 8 | provider-slot count in COMPLIANCE, RISK_SCORE, signed forms | +| `MAX_PROVIDERS_MULTI` | 5 | signer-slot count in COMPLIANCE_MULTI_SIGNED (0x09) | +| `MERKLE_DEPTH` | 20 | tree depth for MEMBERSHIP, NON_MEMBERSHIP, ATTESTATION | +| `MAX_TRANSACTIONS` | 16 | transaction-slot count in PATTERN | +| `MAX_WEIGHT` | 10000 | per-provider weight ceiling (overflow guard on the score) | + +**Value ranges.** Each per-provider screening signal is a `u32` in `[0, 100]`. Each weight is a `u32` in `[0, MAX_WEIGHT]`. `weight_sum` is `u32`, strictly positive. `num_providers` is `u32` in `[1, MAX_PROVIDERS]`. `num_transactions` is `u32` in `[1, MAX_TRANSACTIONS]`. Jurisdiction IDs are `u8` in `[0, 3]` per Jurisdiction Configuration. In COMPLIANCE_MULTI_SIGNED (0x09), per-slot signal range is not enforced in-circuit because the per-slot signature already attests to the signed values; signers MUST sign only signals in `[0, 100]`. + +**Public input encoding.** Every public input is a single field element of the proof system's scalar field (a 254-bit prime field for UltraHonk over BN254). Booleans encode as field `0` (false) or `1` (true). Ethereum addresses encode as the address packed into the low 160 bits of a field element. `u8`, `u32`, and `u64` values encode in the low bits with the high bits zero. Public-input arrays declared by the circuit's `main` (e.g., the five `signer_pubkey_hash` slots in 0x09) appear as one field element per array entry, in declared order. + +**Active-vs-inactive slots.** Circuits with a fixed slot array (`MAX_PROVIDERS` or `MAX_TRANSACTIONS`) and a runtime active count MUST enforce: for `i < count`, the slot carries valid data and a non-zero identifier; for `i >= count`, every per-slot field MUST be zero. This prevents inactive slots from contributing nonzero values to a commitment hash. In COMPLIANCE_MULTI_SIGNED, a signer slot is "active" iff its public `signer_pubkey_hash` is non-zero; inactive slots may carry arbitrary private witness but their constraints are gated by the active flag. + +**Domain-tag distinctness.** The reference implementation uses eight distinct domain tags, prepended to the Pedersen hash input array: one each for internal Merkle nodes, set-bound leaves, value leaves, subject-bound leaves, credential hashes, signed payload, multi-signed slot payload, and signer pubkey commitment. Three other commitments (provider set, config, transaction set) are fixed-arity Pedersen hashes over a single context and do not carry a separate domain tag; the input layout itself is unique to each context. Implementations MAY choose different field values for the eight tags but MUST keep them pairwise distinct and distinct from any field value reachable as a circuit input. + +**Jurisdiction threshold lookup.** Define `highThreshold(jurisdictionId)` to return the high-risk threshold (in basis points) from the table in Jurisdiction Configuration: `EU=7100`, `US=6600`, `UK=7100`, `SG=7600`. + +### Commitment Layouts + +All commitment hashes use the same Pedersen hash primitive over the proof system's scalar field. Each layout below lists field positions left-to-right; "`||`" denotes concatenation into the input array. + +- **`H_provider_set(provider_ids[N], weights[N])`** with `N = MAX_PROVIDERS = 8`. Input is a 16-entry array: `[provider_ids[0], weights[0], provider_ids[1], weights[1], ..., provider_ids[7], weights[7]]`. Inactive slots contribute `(0, 0)` pairs. +- **`H_config(weights[N])`** with `N = MAX_PROVIDERS = 8`. Input is the 8-entry weights array, with `u32` weights packed into field elements. +- **`H_tx_set(amounts[16], timestamps[16])`**. Input is a 32-entry array `[amounts[0], timestamps[0], ..., amounts[15], timestamps[15]]` with `u64` values packed into field elements. Inactive slots contribute `(0, 0)`. +- **`H_credential(provider_id, submitter, credential_type, attribute, expiry)`**. Input is `[DOMAIN_CREDENTIAL, provider_id, submitter, credential_type, attribute, expiry]` (6 fields). Binds the credential to a specific submitter at issuance time. +- **Merkle leaves** use one of three domain-tagged layouts per Merkle Tree Domain Separation: `leaf_hash_set(element, set_id) = H(DOMAIN_LEAF_SET, element, set_id)`; `leaf_hash_value(value) = H(DOMAIN_LEAF_VALUE, value)`; `leaf_hash_subject(value, set_id, salt) = H(DOMAIN_LEAF_SUBJECT, value, set_id, salt)`. Internal nodes are `H(DOMAIN_INTERNAL, left, right)`. +- **`H_signed(chain_id, oracle_address, provider_set_hash, signals[8], weights[8], timestamp, submitter)`**. Used by COMPLIANCE_SIGNED (0x07) and RISK_SCORE_SIGNED (0x08). Input is a 22-entry array: `[DOMAIN_SIGNED_SIGNALS, chain_id, oracle_address, provider_set_hash, signals[0..8], weights[0..8], timestamp, submitter]`. The provider's secp256k1 ECDSA signature is over the 32-byte big-endian serialization of this hash. +- **`H_multi_slot(slot_index, chain_id, oracle_address, jurisdiction_id, provider_set_hash, config_hash, signals[8], weights[8], timestamp, submitter)`**. Used by COMPLIANCE_MULTI_SIGNED (0x09). Input is a 25-entry array: `[DOMAIN_MULTI_SIGNED_SIGNALS, slot_index, chain_id, oracle_address, jurisdiction_id, provider_set_hash, config_hash, signals[0..8], weights[0..8], timestamp, submitter]`. Each active slot's signature is over this digest with its own `slot_index`. The distinct domain tag prevents a 0x07 signature from satisfying a 0x09 slot. +- **`H_signer_pubkey(pubkey_x, pubkey_y)`**. Used to commit to a secp256k1 signing key. Each 32-byte coordinate splits into a high 16-byte half and a low 16-byte half (because the BN254 scalar field cannot represent an arbitrary 256-bit integer). Input is `[DOMAIN_SIGNER_PUBKEY, x_hi, x_lo, y_hi, y_lo]` (5 fields). The result is what Oracle administrators register via `registerSignerPubkeyHash`. + +### Per-Type Circuit Specifications + +Each circuit's `main` function declares the public and private inputs below and enforces the listed constraints. Constraint numbers are normative; their order is for readability. Cross-cutting requirements (active-slot invariants, timestamp bounds, `submitter != 0`) are stated in [Circuit Conventions](#circuit-conventions) and [Circuit Constraints](#circuit-constraints). + +#### COMPLIANCE (0x01) + +Public inputs (6, in order): `jurisdiction_id` (u8), `provider_set_hash`, `config_hash`, `timestamp` (u64), `meets_threshold` (bool), `submitter` (uint160 packed). + +Private inputs: `signals[8]` (u32 each in [0, 100]), `weights[8]` (u32 each <= MAX_WEIGHT), `weight_sum` (u32 > 0), `provider_ids[8]` (Field), `num_providers` (u32 in [1, 8]). + +Constraints: + +1. Active-slot invariant per [Circuit Conventions](#circuit-conventions). +2. `weight_sum == sum(weights[0..8])` (sum over the full array; inactive slots contribute zero). +3. `provider_set_hash == H_provider_set(provider_ids, weights)`. +4. `config_hash == H_config(weights)`. +5. Risk score `s = floor( (sum_{i=0..8} signals[i] * weights[i]) * 100 / weight_sum )`. +6. `meets_threshold == (s < highThreshold(jurisdiction_id))`. +7. `timestamp` satisfies [Circuit Constraints](#circuit-constraints). + +#### RISK_SCORE (0x02) + +Public inputs (8, in order): `proof_type` (u8, 1 = threshold, 2 = range), `direction` (u8, 1 = GT, 2 = LT; ignored for range), `bound_lower` (u32 bps), `bound_upper` (u32 bps; 0 for threshold), `result` (bool), `config_hash`, `provider_set_hash`, `submitter`. + +Private inputs: same as COMPLIANCE. + +Constraints: 1-5 identical to COMPLIANCE (substituting `provider_set_hash` and `config_hash` from this circuit's public inputs). Then: + +6. If `proof_type == 1` and `direction == 1`: `result == (s > bound_lower)`. +7. If `proof_type == 1` and `direction == 2`: `result == (s < bound_lower)`. +8. If `proof_type == 2`: `bound_upper >= bound_lower` and `result == (bound_lower <= s <= bound_upper)`. +9. Reject any other `(proof_type, direction)` combination. + +The Oracle also rejects trivially-true claims (`bound_lower == 0` for direction GT, `bound_lower >= 10000` for direction LT, full-domain ranges) per [Public Input Validation](#public-input-validation). + +#### PATTERN (0x03) + +Public inputs (7, in order): `analysis_type` (u8, 1 = anti-structuring, 2 = velocity, 3 = round-amount), `result` (bool, true = clean), `reporting_threshold` (u64), `time_window` (u64), `tx_set_hash`, `submitter`, `settlement_root`. + +Private inputs: `amounts[16]` (u64), `timestamps[16]` (u64), `num_transactions` (u32 in [1, 16]). + +Constraints: + +1. Active-slot invariant per [Circuit Conventions](#circuit-conventions) (inactive slots: amount = timestamp = 0). +2. `tx_set_hash == H_tx_set(amounts, timestamps)`. +3. `reporting_threshold > 0` and bounded to prevent overflow of any per-analysis arithmetic. +4. `time_window > 0`. +5. For each active slot `i`, `timestamps[i]` satisfies [Circuit Constraints](#circuit-constraints). +6. `result == P(analysis_type, amounts, timestamps, num_transactions, reporting_threshold, time_window)` for an implementation-defined deterministic predicate `P` that is one of three families: + - `analysis_type == 1` (anti-structuring): predicate over `amounts` and `reporting_threshold`. + - `analysis_type == 2` (velocity): predicate over `timestamps`, `num_transactions`, and `time_window`. + - `analysis_type == 3` (round-amount): predicate over `amounts` and `num_transactions`. +7. Reject any other `analysis_type`. +8. `settlement_root` is opaque: the circuit MUST NOT constrain it. Downstream consumers recompute the expected value and assert equality off-circuit. + +Implementations MUST publish the exact predicate parameters they use (e.g., the structuring floor percentage, the velocity max, the round divisor) so verifiers across deployments can be compared. + +#### ATTESTATION (0x04) + +Public inputs (6, in order): `provider_id`, `credential_type` (u8 in [1, 4]; 1 = KYC basic, 4 = institutional, 2-3 reserved), `is_valid` (bool), `credential_root`, `current_timestamp` (u64), `submitter`. + +Private inputs: `credential_attribute` (Field), `expiry_timestamp` (u64), `merkle_index` (Field), `merkle_path[MERKLE_DEPTH]` (Field). + +Constraints: + +1. `credential_hash = H_credential(provider_id, submitter, credential_type, credential_attribute, expiry_timestamp)`. +2. `leaf = leaf_hash_value(credential_hash)`. +3. `compute_merkle_root(leaf, merkle_index, merkle_path) == credential_root`. +4. `is_valid == (current_timestamp < expiry_timestamp) AND (credential_type >= 1 AND credential_type <= 4)`. + +The Oracle also validates that `credential_root` is registered against the proof's `provider_id` per [Validation Registries](#validation-registries). + +#### MEMBERSHIP (0x05) + +Public inputs (5, in order): `merkle_root`, `set_id`, `timestamp` (u64), `is_member` (bool), `submitter`. + +Private inputs: `subject_salt` (Field; 0 for public sets), `merkle_index` (Field), `merkle_path[MERKLE_DEPTH]`. + +Constraints: + +1. `leaf = leaf_hash_subject(submitter, set_id, subject_salt)`. +2. `is_member == (compute_merkle_root(leaf, merkle_index, merkle_path) == merkle_root)`. +3. `timestamp` satisfies [Circuit Constraints](#circuit-constraints). + +#### NON_MEMBERSHIP (0x06) + +Public inputs (5, in order): `merkle_root`, `set_id`, `timestamp` (u64), `is_non_member` (bool), `submitter`. + +Private inputs: `low_leaf` (Field), `low_leaf_salt`, `low_index`, `low_path[MERKLE_DEPTH]`, `high_leaf`, `high_leaf_salt`, `high_index`, `high_path[MERKLE_DEPTH]`. + +Constraints: + +1. `low_leaf_hash = leaf_hash_subject(low_leaf, set_id, low_leaf_salt)` and `high_leaf_hash = leaf_hash_subject(high_leaf, set_id, high_leaf_salt)`. +2. `compute_merkle_root(low_leaf_hash, low_index, low_path) == merkle_root`. +3. `compute_merkle_root(high_leaf_hash, high_index, high_path) == merkle_root`. +4. `low_leaf < submitter < high_leaf` (full-field comparison via bit-decomposition; see [Non-Membership Proof Security](#non-membership-proof-security)). +5. `high_index == low_index + 1` (adjacency). +6. `is_non_member == (clause 2 AND clause 3 AND clause 4 AND clause 5)`. +7. `timestamp` satisfies [Circuit Constraints](#circuit-constraints). + +#### COMPLIANCE_SIGNED (0x07) + +Public inputs (9, in order): the 6 COMPLIANCE inputs in the same order, then `signer_pubkey_hash`, `chain_id`, `oracle_address`. + +Private inputs: the COMPLIANCE private inputs, plus `signature` (64 bytes; secp256k1 ECDSA in raw `r || s` form), `pubkey_x` (32 bytes), `pubkey_y` (32 bytes). + +Constraints: all COMPLIANCE constraints (1-7), plus: + +8. `H_signer_pubkey(pubkey_x, pubkey_y) == signer_pubkey_hash`. +9. `digest = H_signed(chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`. +10. `ecdsa_secp256k1_verify(pubkey_x, pubkey_y, signature, digest_be_bytes) == true`, where `digest_be_bytes` is the 32-byte big-endian serialization of `digest`. + +The Oracle also validates `signer_pubkey_hash` is registered, `chain_id == block.chainid`, and `oracle_address == address(this)` per [Public Input Validation](#public-input-validation). + +#### RISK_SCORE_SIGNED (0x08) + +Public inputs (11, in order): the 8 RISK_SCORE inputs, then `signer_pubkey_hash`, `chain_id`, `oracle_address`. + +Private inputs: the RISK_SCORE private inputs, plus `signature`, `pubkey_x`, `pubkey_y` (as in 0x07), plus `signed_timestamp` (Field) — used in the signed digest because RISK_SCORE has no public timestamp. + +Constraints: RISK_SCORE constraints (1-9), plus: + +10. `H_signer_pubkey(pubkey_x, pubkey_y) == signer_pubkey_hash`. +11. `digest = H_signed(chain_id, oracle_address, provider_set_hash, signals, weights, signed_timestamp, submitter)`. +12. `ecdsa_secp256k1_verify(pubkey_x, pubkey_y, signature, digest_be_bytes) == true`. + +#### COMPLIANCE_MULTI_SIGNED (0x09) + +Public inputs (14, in order): `jurisdiction_id` (u8), `provider_set_hash`, `config_hash`, `timestamp`, `meets_threshold` (bool), `threshold_m` (u8), `signer_pubkey_hash_0`, `signer_pubkey_hash_1`, `signer_pubkey_hash_2`, `signer_pubkey_hash_3`, `signer_pubkey_hash_4`, `chain_id`, `oracle_address`, `submitter`. + +Private inputs: per-slot arrays of size 5, each with `signals[8]`, `weights[8]`, `weight_sum`, `pubkey_x[32]`, `pubkey_y[32]`, `signature[64]`. Inactive slots set `weight_sum = 1`, `weights = [1, 0, ...]`, `signals = [0; 8]` so the per-slot score arithmetic remains well-defined. + +Constraints: for each slot `i` in `0..MAX_PROVIDERS_MULTI`: + +1. Let `active_i = (signer_pubkey_hash_i != 0)`. +2. `weight_sum_i > 0` and `weight_sum_i == sum_j weights_i[j]`. Enforced for all slots (active and inactive) so the per-slot score arithmetic is well-defined and the denominator cannot be inflated. +3. `slot_score_i = floor( (sum_j signals_i[j] * weights_i[j]) * 100 / weight_sum_i )`. +4. `slot_digest_i = H_multi_slot(i, chain_id, oracle_address, jurisdiction_id, provider_set_hash, config_hash, signals_i, weights_i, timestamp, submitter)`. +5. If `active_i`: `H_signer_pubkey(pubkey_x_i, pubkey_y_i) == signer_pubkey_hash_i`; `ecdsa_secp256k1_verify(pubkey_x_i, pubkey_y_i, signature_i, slot_digest_i_be_bytes) == true`; `slot_score_i < highThreshold(jurisdiction_id)`. +6. Inactive slot constraints (signature check, score floor check) are gated on `!active_i` and accept arbitrary witness. + +Cross-slot: + +7. `count(active_i for i in 0..5) >= threshold_m` (and `threshold_m in [1, MAX_PROVIDERS_MULTI]`). +8. All non-zero `signer_pubkey_hash_i` MUST be pairwise distinct (no signer fills two slots). +9. `meets_threshold == true` (encoded as field `1`). A valid proof cannot be produced with `meets_threshold = false`: the active-slot floor checks and signature checks in step 5 are hard asserts, so any failure prevents the proof from existing. The public field exists for layout parity with 0x07 and so the Oracle can route on it. +10. `timestamp` satisfies [Circuit Constraints](#circuit-constraints). + +The Oracle also validates `threshold_m >= JurisdictionConfig.minMultiProviderThreshold(jurisdiction_id)`, each non-zero `signer_pubkey_hash_i` is registered, `chain_id == block.chainid`, and `oracle_address == address(this)` per [Public Input Validation](#public-input-validation). + ### Verifier Interface The verifier routes proof verification to per-proof-type verification contracts. Each circuit produces a separate verifier via the ZK backend (e.g., `bb write_solidity_verifier` for Barretenberg's UltraHonk). @@ -134,6 +323,12 @@ interface IERC8262Verifier { Implementations MUST also implement [ERC-165](./eip-165.md). `supportsInterface(bytes4)` MUST return `true` for `type(IERC8262Verifier).interfaceId` and for `type(IERC165).interfaceId`, and `false` for `0xffffffff`. +Each per-circuit verifier's `verify(bytes, bytes32[])` function MUST be declared `view` so that the EVM uses `STATICCALL` when the router invokes it. Implementations MUST NOT invoke verifiers via interfaces that omit the `view` modifier; a non-`view` verifier could reenter the calling Oracle and mutate attestation state mid-verification. + +### Batch Verification Limits + +Implementations MUST enforce a maximum batch size for `verifyProofBatch` and `submitComplianceBatch` to bound worst-case gas consumption. The cap MUST be chosen so a full batch fits comfortably within the target chain's block gas limit with headroom for the submission overhead (registry lookups, replay-guard SSTORE, event emission). The reference implementation caps both at 10, sized for the 30 M-gas mainnet ceiling; deployments on chains with larger or smaller block budgets MUST recalibrate. + ### Oracle Interface ```solidity @@ -268,6 +463,16 @@ interface IERC8262Oracle { Implementations MUST also implement [ERC-165](./eip-165.md). `supportsInterface(bytes4)` MUST return `true` for `type(IERC8262Oracle).interfaceId` and for `type(IERC165).interfaceId`, and `false` for `0xffffffff`. +### Proof Hash Computation + +Implementations MUST compute the `proofHash` field of `ComplianceAttestation` as: + +``` +proofHash = keccak256(abi.encodePacked(proof, proofType, block.chainid, address(this))) +``` + +Including `proofType` scopes uniqueness per proof type, so identical proof bytes submitted under different types are treated as distinct. Including `block.chainid` and `address(this)` prevents on-chain replay across forks or alternate Oracle deployments. This is the on-chain replay guard only; in-circuit chain binding is provided by the signed variants (see [Public Input Validation](#public-input-validation)). + ### Jurisdiction Configuration Implementations MUST publish jurisdiction thresholds openly. Risk scores are expressed in basis points (0-10000 = 0.00%-100.00%). @@ -294,7 +499,9 @@ Compliance attestations have a configurable time-to-live (TTL): Implementations SHOULD publish provider weights as an on-chain configuration hash. Weight changes MUST emit `ProviderWeightsUpdated` with the new configuration hash, timestamp, and an optional `metadataURI` pointing to the full configuration (e.g., on IPFS or Arweave). -Provider configuration MUST be versioned. Implementations SHOULD maintain a history of configuration hashes to support retroactive verification: determining which weights were active when a particular proof was generated. Implementations SHOULD support revoking historical configuration hashes when a configuration is discovered to be flawed. The currently active configuration MUST NOT be revocable. +Provider configuration MUST be versioned. Implementations SHOULD maintain a history of configuration hashes to support retroactive verification: determining which weights were active when a particular proof was generated. Implementations SHOULD support revoking historical configuration hashes when a configuration is discovered to be flawed. The currently active configuration MUST NOT be revocable. Implementations MUST permanently retain revocation status: a previously-revoked configuration hash MUST NOT be re-registrable, to prevent silent un-revocation. + +Configuration history SHOULD be bounded to prevent unbounded storage growth (e.g., 256 entries, with FIFO eviction of the oldest non-current entries). ### Proof Type Routing @@ -321,6 +528,8 @@ Implementations MUST support revoking a specific historical version when a verif Revocation MAY have two paths: a delayed path (default) and an immediate path gated behind the GUARDIAN role for cases where a paused proof type needs the revocation locked in before the timelock elapses. The reference implementation uses a 6 h delay for the routine path. +Implementations MUST resolve the verifier address once per submission and use that resolved address for both proof verification and the `verifierUsed` field of the recorded attestation. A time-of-check / time-of-use gap between resolution and verification would allow the recorded `verifierUsed` to diverge from the verifier that actually validated the proof, breaking retroactive verification guarantees. + ### Public Input Validation Implementations MUST validate public inputs semantically for each proof type before forwarding to the per-circuit verifier. The ZK proof guarantees internal consistency (e.g., that the score was correctly computed from the committed inputs), but the oracle MUST verify that those committed inputs match the expected context (e.g., that the config hash is a known configuration, that the merkle root belongs to a registered set). Without this validation, a valid proof generated for one context can be replayed in a different context. @@ -358,6 +567,10 @@ Implementations MUST maintain on-chain registries for values that public inputs **Reporting threshold registry.** Tracks valid reporting thresholds for PATTERN (anti-structuring) proofs. Each jurisdiction defines its own reporting threshold (e.g., $10,000 for US BSA). Thresholds MUST be registered before proofs referencing them can be accepted. +**Registry idempotency.** Across all registries, re-registering an already-registered value MUST revert, and revoking a value that is not registered MUST revert. This prevents accidental double-registration and silent no-op revocations from masking a misconfigured admin flow. + +**Credential publisher rotation.** Each per-provider credential root binds to a publisher EOA recorded on-chain. Implementations SHOULD support rotating the publisher EOA under a delayed administrative path (e.g., 6 h timelock) to limit damage from a compromised publisher key, and SHOULD support immediate credential root revocation (no delay) so a malicious root discovered after publication can be removed before its TTL elapses. The currently registered publisher MUST be replaceable only by the owner (not by the publisher itself). + ### Risk Score Computation The risk score formula MUST be deterministic and publicly verifiable: @@ -375,6 +588,12 @@ The ZK proof commits to: - Resulting score (hidden) - Whether jurisdiction threshold was crossed (revealed as boolean) +The risk score's integrity depends on the independence of the contributing providers. Implementations whose threat model includes collusion among a subset of providers SHOULD require attestations from multiple independent providers (e.g., via COMPLIANCE_MULTI_SIGNED with `threshold_m >= 2`) and SHOULD weight providers based on enforcement track record so a single compromised or coerced provider cannot drive the score across a jurisdiction threshold. + +### Circuit Constraints + +Circuits MUST enforce realistic timestamp bounds on the public `timestamp` inputs they consume. A timestamp before 2021-01-01 (UNIX `1609459200`) or after a far-future bound (e.g., UNIX `0xFFFFFFFF`, ~year 2106) MUST be rejected in-circuit. This applies to COMPLIANCE, COMPLIANCE_SIGNED, COMPLIANCE_MULTI_SIGNED, MEMBERSHIP, NON_MEMBERSHIP, and to each active per-transaction timestamp in PATTERN. ATTESTATION's `current_timestamp` and `expiry_timestamp` are not range-bounded in-circuit; freshness is the consumer's responsibility. RISK_SCORE has no timestamp input, and RISK_SCORE_SIGNED's `signed_timestamp` is committed to the provider's signature rather than range-checked. Without these bounds where they apply, a malicious prover could backdate or far-future-date a proof to bypass attestation TTL checks downstream. + ### Hash Function Requirements Circuits MUST use a collision-resistant hash function for all commitments (provider set hashes, config hashes, Merkle trees, credential hashes). The reference implementation uses Pedersen hash, which is efficient in ZK circuits and available in the Noir standard library. @@ -421,6 +640,18 @@ Each compliance proof MUST commit to: This enables proof-of-innocence: counterparties to retroactively flagged addresses can present the original attestation (retrieved via `getHistoricalProof()`) demonstrating the address was clean at transaction time. The on-chain record is immutable and independently verifiable. +### Pause Mechanism + +Implementations SHOULD support pausing proof submission so a discovered circuit or verifier vulnerability can be contained without redeploying. Pause MUST NOT block read access to existing attestations (`checkCompliance`, `checkComplianceByType`, `getHistoricalProof`, `getAttestationHistory`): retroactive verification (proof-of-innocence) depends on those endpoints remaining live during an incident. Implementations SHOULD support per-proof-type pause in addition to global pause so unrelated proof types remain available during a scoped incident response. + +### Administrative Operations + +Verifier replacement, weight updates, registry mutations, TTL changes, and publisher rotations are privileged operations. Implementations MUST use a two-step ownership transfer pattern (`transferOwnership` + `acceptOwnership`) for owner handover to prevent accidental transfer to an incorrect address. Implementations SHOULD timelock critical operations (verifier replacement, TTL changes, weight updates) in production. Implementations SHOULD split administrative authority into role classes with bounded blast radius (for example, a pause-only "guardian" role distinct from registry-mutating and config-mutating roles) so that a single compromised key cannot both pause the Oracle and rewrite its registries. The reference implementation uses a three-role split (GUARDIAN, REGISTRAR, CONFIG) under a 2-tier selector-gated timelock. + +### Trust Tier Disclosure + +Implementations SHOULD publish, in deployment-facing documentation, the trust tier (self-attested, provider-attested, or credential-attested) they accept per jurisdiction, together with the on-chain addresses of the Verifier and Oracle and the list of registered providers and signer pubkey hashes. Integrators can then match a deployment against their threat model without inspecting on-chain state, and external auditors can confirm that the published policy matches the on-chain configuration. + ## Rationale **Why client-side computation?** Server-side or TEE-based compliance creates a trusted party that can be coerced, compromised, or surveilled. Client-side ZK proof generation means the raw data never leaves the user's device. The verifier learns only the boolean result. @@ -437,7 +668,7 @@ This enables proof-of-innocence: counterparties to retroactively flagged address ### What this standard does NOT prove -The single most important caveat for adopters: the cryptographic guarantees in this ERC are about _correct computation_, not about _honest inputs_. Three trust tiers exist across the proof types, and integrators MUST pick the tier that matches their threat model. +The single most important caveat for adopters: the cryptographic guarantees in this ERC are about _correct computation_, not about _honest inputs_. Three trust tiers exist across the proof types, and integrators have to pick the tier that matches their threat model. | Tier | Proof types | Who attests the screening signals? | | ------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------ | @@ -447,9 +678,7 @@ The single most important caveat for adopters: the cryptographic guarantees in t The self-attested tier is useful for jurisdictions that explicitly permit user-asserted compliance (some EU and UK contexts), for fast-path flows where a downstream system performs the honest-signal check, and as a building block in larger composed proofs. A user submitting a self-attested COMPLIANCE proof could in principle pass `signals = [0, ...]` and produce a valid "low-risk" proof regardless of their true screening result; `provider_set_hash` and `config_hash` commit to _which_ providers and weights were used, not to _what_ those providers returned. This is documented as an explicit design tradeoff, not a bug. -Strict-mode jurisdictions (US BSA, Singapore) MUST reject the self-attested tier — the reference enforces this via `JurisdictionConfig.requireSignedSignals(uint8)`. Permissive jurisdictions MAY accept either tier. Integrators whose threat model includes a dishonest user but a trusted provider MUST require the signed variants. Integrators whose threat model includes a compromised provider key MUST additionally require an ATTESTATION proof against an independently-published credential tree, ideally with an in-circuit signature over the credential root (out of scope for this specification, tracked as future work). - -Implementations SHOULD prominently document this trust model in deployment-facing materials. +The strict-mode-jurisdiction policy and the integrator guidance above are mirrored normatively in the Specification (see [Public Input Validation](#public-input-validation) and [Trust Tier Disclosure](#trust-tier-disclosure)); this Rationale section explains the *why*. Adopters benefit from publishing which tier they accept per jurisdiction so integrators can match the deployment's trust posture against their own threat model without inspecting on-chain state. ### Related Work @@ -467,7 +696,7 @@ Several existing and emerging standards address compliance, privacy, or on-chain **Smart-account ZK verifier interface.** A draft ERC proposes a proof-system-agnostic ZK verification interface for smart accounts (`verifyProof(bytes,bytes) returns (bytes4)`), standardizing per-relation verifier contracts with a non-reverting return pattern (following [ERC-1271](./eip-1271.md)). This ERC's per-proof-type verifier routing serves a similar verification role but with domain-specific semantics (proof type routing, batch verification, version history). Each generated UltraHonk verifier in this ERC could be wrapped behind such an adapter for smart account integration. -**[EIP-7702](./eip-7702.md).** Account abstraction via temporary delegation: an EOA can authorize a contract to execute code on its behalf for a single transaction. EIP-7702 interacts with the `submitter == msg.sender` rule in two ways. First, when a 7702-delegated EOA calls `submitCompliance`, `msg.sender` is the EOA address (not the delegated contract), so the attestation correctly binds to the EOA and the `submitter` public input must equal that EOA. Second, a smart-account batcher (using 7702 to wrap multiple operations) can call `submitComplianceBatch` provided every entry's `submitter` public input equals the delegating EOA. Account-abstraction wallets MUST surface the bound `submitter` address to the user before submission, since a malicious dApp could otherwise solicit proofs bound to the wrong address. The same considerations apply to [ERC-4337](./eip-4337.md) paymasters and ERC-1271 contract signers when used as compliance subjects. +**[EIP-7702](./eip-7702.md).** Account abstraction via temporary delegation: an EOA can authorize a contract to execute code on its behalf for a single transaction. EIP-7702 interacts with the `submitter == msg.sender` rule in two ways. First, when a 7702-delegated EOA calls `submitCompliance`, `msg.sender` is the EOA address (not the delegated contract), so the attestation correctly binds to the EOA and the `submitter` public input must equal that EOA. Second, a smart-account batcher (using 7702 to wrap multiple operations) can call `submitComplianceBatch` provided every entry's `submitter` public input equals the delegating EOA. Account-abstraction wallets should surface the bound `submitter` address to the user before submission, since a malicious dApp could otherwise solicit proofs bound to the wrong address. The same considerations apply to [ERC-4337](./eip-4337.md) paymasters and ERC-1271 contract signers when used as compliance subjects. **MultiTrust Credential.** Companion draft ERCs propose non-transferable credential anchors with ZK presentation via fixed Groth16 ABI, supporting predicate proofs ("score >= threshold") without revealing raw data. The predicate-proving pattern parallels this ERC's RISK_SCORE proof type. MultiTrust focuses on credential issuance and presentation; this ERC focuses on compliance attestation and retroactive verification. @@ -612,46 +841,35 @@ A reference implementation accompanies this ERC. It consists of: - **Generated verifiers**: `src/generated/` (UltraHonk verifiers generated by Barretenberg, pinned to bb 4.0.0-nightly.20260120) - **Test suite**: Solidity tests (unit, fuzz, invariant, integration with real proofs for the 6 unsigned proof types) and circuit tests across all 9 circuits. The signed variants (0x07, 0x08, 0x09) are exercised in the TypeScript SDK consumer tests (`test/sdk/`) which generate a fresh ECDSA witness per run. -Thanks to Merkle Bonsai (@Jabher) for reviewing the generated UltraHonk verifiers and identifying that the `pairing()` free function could be rewritten in inline Yul to bring all nine per-proof-type verifiers under the [EIP-170](./eip-170.md) 24,576-byte runtime size limit. The reference implementation incorporates the rewrite in `scripts/patch-pairing-yul.sh`, saving ~186 bytes per verifier (and ~800 gas per `verifyProof` call as a bonus) while staying byte-identical to the `bb`-generated semantics on the pairing precompile (`address(0x08)`) input layout. + -## Security Considerations +Raw `bb`-generated UltraHonk verifiers exceed the [EIP-170](./eip-170.md) 24,576-byte runtime size limit for some of the nine proof types. The reference implementation rewrites the `pairing()` free function in inline Yul (`scripts/patch-pairing-yul.sh`), saving ~186 bytes per verifier (and ~800 gas per `verifyProof` call as a bonus) while staying byte-identical to the `bb`-generated semantics on the pairing precompile (`address(0x08)`) input layout. -**Proof soundness.** The security of the system depends on the ZK proof system used. Implementations MUST use a proof system with at least 128-bit security. Groth16, PLONK, and UltraHonk (Noir/Aztec) are acceptable. +## Security Considerations -**Provider collusion.** If all screening providers collude, they could issue false clean signals. Implementations SHOULD require attestations from multiple independent providers and weight them based on enforcement track record. +This section discusses the threat model behind the normative requirements in the Specification. Each subsection names the threat, identifies the affected proof types, points at the Spec subsection that mandates the mitigation, and discusses residual risk. -**Timestamp manipulation.** Proofs commit to block timestamps. Block proposers control the timestamp, constrained only to be >= the parent block's timestamp. This is acceptable for compliance windows measured in days. Circuits MUST enforce realistic timestamp bounds (e.g., after 2021-01-01 and before year ~36000) to reject obviously invalid values. This applies to both public timestamp inputs (compliance, membership, non-membership) and private transaction timestamps (pattern). +**Proof soundness.** End-to-end security collapses to the soundness of the underlying ZK proof system. A weak proof system would let a prover forge a passing attestation without satisfying the circuit constraints; no other defense in this standard saves it. The 128-bit-security floor in [Proof System Requirements](#proof-system-requirements) is set by the strongest practical attack on Groth16/PLONK/UltraHonk over BN254-class curves. -**Regulatory acceptance.** This standard provides a technical mechanism for ZK compliance. Whether specific jurisdictions accept ZK proofs as sufficient compliance evidence is a legal question, not a technical one. The VARA (Dubai) definition of "anonymity-enhanced crypto" excludes assets with "mitigating technologies" for traceability. This standard provides exactly that technology. +**Provider collusion.** If a majority of weighted providers collude (or if a single provider is the sole source under unsigned COMPLIANCE), they can issue false clean signals and the protocol cannot detect it. [Risk Score Computation](#risk-score-computation) recommends multi-provider attestation; COMPLIANCE_MULTI_SIGNED (0x09) with `threshold_m >= 2` is the strongest in-protocol defense, and per-jurisdiction `minMultiProviderThreshold` provides a deployment-level floor. -**Front-running the oracle.** Compliance proofs are generated before settlement. An adversary who observes a proof submission could infer a trade is about to occur. Implementations SHOULD batch proof submissions or submit them as part of the settlement transaction to minimize information leakage. +**Timestamp manipulation.** Block proposers control `block.timestamp` within the parent-block constraint. This is acceptable for compliance windows measured in days but unacceptable as a fine-grained source of ordering or liveness. [Circuit Constraints](#circuit-constraints) requires realistic bounds on every timestamp input. Integrators that need finer-grained ordering must use an explicit nonce or sequence number, not the timestamp. -**Administrative operations.** Verifier contract updates and provider weight changes are privileged operations. Implementations SHOULD use a two-step ownership transfer pattern (transferOwnership + acceptOwnership) to prevent accidental transfer to an incorrect address. Critical operations (verifier replacement, TTL changes) SHOULD be timelocked in production deployments. Implementations SHOULD further split administrative authority into role classes with bounded blast radius -- for example, a pause-only "guardian" role distinct from registry-mutating and config-mutating roles -- so that a single compromised key cannot both pause and rewrite registries. The reference implementation uses a three-role split (GUARDIAN, REGISTRAR, CONFIG) under a 2-tier selector-gated timelock; see `docs/THREAT_MODEL.md` in the reference implementation repository for the full per-role capability matrix and the timelock-tier mapping. +**Regulatory acceptance.** This standard provides a technical mechanism for ZK compliance. Whether specific jurisdictions accept ZK proofs as sufficient compliance evidence is a legal question, not a technical one. The VARA (Dubai) definition of "anonymity-enhanced crypto" excludes assets with "mitigating technologies" for traceability. This standard provides exactly that technology. -**Public input validation.** Implementations MUST validate public inputs for every proof type, not just the primary compliance proof. Without validation, a prover can generate a proof for one context (e.g., a lenient jurisdiction's reporting threshold) and submit it for a different context. Specifically: +**Front-running the oracle.** Compliance proofs are generated before settlement. An adversary who observes a proof submission in the mempool can infer that a trade is about to occur, even though the trade details remain hidden. Integrators that want to minimize this information leakage can batch proof submissions or piggyback proof submission on the settlement transaction itself; this is a deployment-policy choice, not a normative requirement of the standard. -- ALL proof types MUST validate their boolean result field (`meets_threshold`, `result`, `is_valid`, `is_member`, `is_non_member`) equals `bytes32(uint256(1))`. A valid proof with a false result proves non-compliance; accepting it would record a compliant attestation for a non-compliant subject. -- ALL proof types MUST enforce `submitter == msg.sender` to prevent submission front-running. -- COMPLIANCE and RISK_SCORE proofs MUST validate `config_hash` against a registry of known configurations. -- COMPLIANCE proofs MUST validate `jurisdiction_id` and `provider_set_hash` against caller-supplied parameters. -- RISK_SCORE proofs commit to `provider_set_hash` as a public input, binding the proof to a specific set of screening providers. This prevents a prover from fabricating signals from unverified providers. -- RISK_SCORE proofs MUST validate the semantic public inputs (`proof_type ∈ {threshold, range}`, `direction ∈ {GT, LT}`, and bounds) to reject trivially-true claims. For example, a THRESHOLD/GT proof with `bound_lower = 0` proves only that the score is greater than zero, which is uninformative. Validators MUST reject such proofs. -- PATTERN (anti-structuring) proofs MUST validate `reporting_threshold` against a per-jurisdiction registry, enforce `time_window >= MIN_TIME_WINDOW`, and validate that `analysis_type` is one of the supported analyses. Implementations that depend on a specific analysis type (e.g., a settlement registry requiring anti-structuring) MUST verify the `analysis_type` field separately, since the result boolean alone is insufficient. -- ATTESTATION proofs MUST validate `credential_root` against the per-provider credential root registry AND verify the registry's recorded `providerId` matches the proof's `provider_id` public input. Without this cross-check, a proof for one provider's tree could be replayed against another provider's registration. -- MEMBERSHIP and NON_MEMBERSHIP proofs MUST validate `merkle_root` against the generic merkle root registry. -- COMPLIANCE_SIGNED and RISK_SCORE_SIGNED proofs MUST validate `signer_pubkey_hash` against an on-chain registry of authorized provider signing keys, AND MUST validate `chain_id == block.chainid` and `oracle_address == address(this)`. Without the chain/oracle binding, a single provider signature could mint attestations across alternate Oracle deployments (different chain, or a forked Oracle on the same chain). -- Strict-mode jurisdictions SHOULD reject the unsigned siblings entirely and accept only the signed variants. The reference implementation enforces this for US (BSA) and Singapore via `JurisdictionConfig.requireSignedSignals(uint8)`. -- Unknown proof types (outside 0x01-0x09) MUST be rejected. +**Administrative operations.** Verifier replacement, weight updates, registry mutations, TTL changes, and publisher rotations are the most consequential privileged operations. A single compromised admin key could pause the Oracle and rewrite its registries in one transaction. [Administrative Operations](#administrative-operations) mandates two-step ownership transfer and recommends splitting authority into guardian, registrar, and config roles behind a timelock. The reference implementation's per-role capability matrix and timelock-tier mapping is in `docs/THREAT_MODEL.md` in the reference implementation repository. -**Proof replay prevention.** Proof hashes MUST be keyed on the proof bytes, the proof type, and the deployment context: `keccak256(abi.encodePacked(proof, proofType, block.chainid, address(this)))`. Including `proofType` scopes uniqueness per proof type (identical proof bytes submitted for different proof types are treated as distinct proofs); including `block.chainid` and `address(this)` prevents replay-into-storage from a forked or alternate Oracle deployment on the same or different chain, even if the underlying ZK proof is chain-agnostic. Note this is the on-chain replay guard only; see "Cross-chain replay" below for in-circuit chain binding (relevant to the signed variants). +**Public input validation.** Without validating that each public input matches a registered on-chain context, a prover can replay a proof produced for a lenient context (e.g., a permissive jurisdiction's reporting threshold) into a stricter one. [Public Input Validation](#public-input-validation) defines the per-proof-type validation matrix. The most subtle case is the boolean result field: a ZK proof carrying `meets_threshold = false` (or `result = false`, `is_valid = false`, `is_member = false`, `is_non_member = false`) is a valid *proof of non-compliance*. Accepting it without checking the result would record a compliant attestation for a non-compliant subject. [Proof Result Validation](#proof-result-validation) covers this explicitly. -**Config and root revocation.** Provider configuration hashes and merkle roots SHOULD be revocable. Without revocation, a discovered-to-be-flawed configuration or a compromised merkle tree remains accepted forever. Implementations MUST NOT allow revoking the currently active provider configuration. Provider configuration history SHOULD be bounded to prevent unbounded storage growth (e.g., 256 entries). +**Proof replay prevention.** The proof-hash formula in [Proof Hash Computation](#proof-hash-computation) scopes attestation storage to a single `(chain, Oracle, proofType)` triple. Identical proof bytes submitted under different proof types or against different Oracle deployments are treated as independent, and the `_usedProofs` guard inside one Oracle prevents the same proof from being re-submitted. This guard is on-chain only; in-circuit chain/Oracle binding for the signed variants is handled by [Public Input Validation](#public-input-validation). -**Verifier TOCTOU.** Implementations MUST resolve the verifier address once per submission and use it for both proof verification and attestation recording. A time-of-check/time-of-use gap between address resolution and proof verification could allow the recorded `verifierUsed` to diverge from the actual verifier if a verifier upgrade occurs mid-transaction. +**Config and root revocation.** Without revocation, a flawed configuration or a compromised merkle tree discovered after publication remains accepted forever. [Provider Weight Publication](#provider-weight-publication) and [Validation Registries](#validation-registries) define the revocation rules: the currently active provider configuration cannot be revoked (revocation must proceed by registering a replacement first); revocation status is permanent (a previously-revoked hash cannot be re-registered); and configuration history is bounded so storage cannot grow without bound. -**Batch verification limits.** Implementations MUST enforce a maximum batch size for `verifyProofBatch()` to prevent unbounded gas consumption. The reference implementation uses a limit of 10 proofs per batch, sized to fit comfortably under a 30M-gas mainnet block ceiling (approximately 24M gas at the maximum batch size, with ~5M gas of headroom). Implementations targeting chains with different block-gas budgets SHOULD recalibrate the limit accordingly. +**Verifier TOCTOU.** [Verifier Versioning](#verifier-versioning) requires verifier-address resolution to happen exactly once per submission, with the resolved address used for both verification and the recorded `verifierUsed` field. Without this, a verifier upgrade landing mid-transaction could record a different verifier than the one that actually validated the proof, breaking retroactive verification guarantees. -**Expected gas costs.** UltraHonk verification dominates total transaction cost. The following figures are measured against the reference implementation on the Cancun EVM with real proofs (see `test/GasBenchmark.t.sol` in the reference implementation repository); other proof systems and circuit revisions will differ. +**Batch verification limits.** Batched verification multiplies the per-proof cost (~2.4 M gas for UltraHonk) by the batch size. [Batch Verification Limits](#batch-verification-limits) requires an enforced cap sized for the target chain's block gas limit. The reference figures below show the linear cost growth on Cancun EVM with real proofs (see `test/GasBenchmark.t.sol` in the reference implementation repository); other proof systems and circuit revisions will differ. | Operation | Approx. gas | | ----------------------------------------------------- | ----------- | @@ -662,23 +880,19 @@ Thanks to Merkle Bonsai (@Jabher) for reviewing the generated UltraHonk verifier | ... 5 entries | ~12.05M | | ... 10 entries (max batch) | ~24.08M | -Signed-variant gas (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) is dominated by in-circuit ECDSA-secp256k1 verification, which roughly doubles proving time off-chain but only modestly increases the verifier byte size; on-chain `verifyProof` for the signed variants is in the same order of magnitude as the unsigned variants. Implementations that target L2s (typical block target 30M-60M gas) MAY raise `MAX_BATCH_SIZE` proportionally. Implementations on chains with lower block-gas budgets MUST lower it. - -Submission overhead beyond verification (~400-470k gas per attestation) covers public-input validation, registry lookups, replay-guard SSTORE, attestation storage, and the `ComplianceVerified` event. Integrators submitting many attestations per user can amortize the per-entry fixed cost via `submitComplianceBatch`. - -**Registry idempotency.** Registry operations (registering merkle roots, reporting thresholds) SHOULD be idempotent-safe: re-registering an already-registered value SHOULD revert to prevent accidental double-registration. Similarly, revoking a value that is not registered SHOULD revert. +Signed-variant gas (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) is dominated by in-circuit ECDSA-secp256k1 verification, which roughly doubles proving time off-chain but only modestly increases the verifier byte size; on-chain `verifyProof` for the signed variants is in the same order of magnitude as the unsigned variants. Implementations targeting L2s with larger block budgets can raise the batch cap proportionally; implementations on chains with lower budgets must lower it. Submission overhead beyond verification (~400-470 k gas per attestation) covers public-input validation, registry lookups, the replay-guard SSTORE, attestation storage, and event emission. Integrators submitting many attestations per user can amortize the per-entry fixed cost via `submitComplianceBatch`. -**Emergency circuit break.** Implementations SHOULD include a pause mechanism that can halt proof submissions (and optionally, verifications) in case of a discovered vulnerability in a ZK circuit or verifier contract. Pausing MUST NOT prevent read access to existing attestations, as these are needed for retroactive verification (proof-of-innocence). Implementations SHOULD support per-proof-type pause for surgical incident response without halting unrelated proof types. +**Registry idempotency.** A silent no-op on a duplicate registration or a missing revocation could mask a misconfigured admin flow and leave operators believing a state change landed when it did not. [Validation Registries](#validation-registries) requires both operations to revert in those cases. -**Trust model and signal honesty.** See [What this standard does NOT prove](#what-this-standard-does-not-prove) in Rationale for the full discussion. In short: the unsigned variants (COMPLIANCE, RISK_SCORE) are self-attested — the circuit verifies the score formula but not the screening signals themselves. Strict-mode jurisdictions MUST reject the unsigned variants; integrators in permissive jurisdictions that require signal honesty MUST require COMPLIANCE_SIGNED / RISK_SCORE_SIGNED, optionally composed with ATTESTATION proofs. +**Emergency circuit break.** A discovered circuit or verifier vulnerability needs to be contained without redeploying. [Pause Mechanism](#pause-mechanism) mandates a pause that halts submission while keeping reads available, since proof-of-innocence depends on `getHistoricalProof` and `checkCompliance` staying live. Per-proof-type pause limits incident blast radius to the affected circuit. -**ATTESTATION authority root.** ATTESTATION proofs verify Merkle inclusion of a credential leaf in a per-provider credentials tree. The leaf is `H(DOMAIN_CREDENTIAL, provider_id, submitter, type, attribute, expiry)`, which binds the credential to the submitter cryptographically; a forged credential leaf cannot be constructed without breaking Pedersen preimage resistance. However, the circuit does NOT verify a provider signature over the credential leaf or root in-circuit. Authority resolves to the registered publisher EOA: the Oracle records each `credentialRoot` against `(providerId, publisherEOA)`, with a 48 h root TTL and an owner-rotatable publisher. A compromised publisher EOA can publish a tree containing arbitrary `(submitter, attribute)` pairs until the owner rotates the publisher (`setProviderPublisher`, 6 h timelock) or revokes the root (`revokeCredentialRoot`, instant). Implementations whose threat model includes a compromised publisher key SHOULD layer an in-circuit signature scheme over the credential root or credential leaf; this is tracked as future work and is intentionally NOT required by this specification. +**Trust model and signal honesty.** See [What this standard does NOT prove](#what-this-standard-does-not-prove) in Rationale for the full discussion. In short: the unsigned variants (COMPLIANCE, RISK_SCORE) are self-attested. The circuit verifies the score formula but not the screening signals themselves. The per-jurisdiction policy in [Public Input Validation](#public-input-validation) rejects the unsigned siblings for strict-mode jurisdictions (US BSA, Singapore in the reference implementation). Integrators in permissive jurisdictions that need signal honesty should require the signed variants, optionally composed with ATTESTATION proofs against an independently-published credential tree. [Trust Tier Disclosure](#trust-tier-disclosure) lets integrators read the deployment's accepted tier without inspecting on-chain state. -**Cross-chain replay.** The unsigned proof types (COMPLIANCE, RISK_SCORE, PATTERN, ATTESTATION, MEMBERSHIP, NON_MEMBERSHIP) do NOT include a chain identifier as a circuit public input. The same proof bytes may be replayed against the same Oracle on a different chain (or against an alternate Oracle deployment on the same chain), producing independent attestations on each. The on-chain `proofHash = keccak256(proof, proofType, block.chainid, address(this))` and the `_usedProofs[proofHash]` guard prevent replay-into-storage _within_ a given (chain, Oracle) pair, but provide no in-circuit binding. Implementations whose threat model includes cross-deployment replay of unsigned proofs MUST add `chainId` and the verifying contract address as circuit public inputs. +**ATTESTATION authority root.** ATTESTATION proofs verify Merkle inclusion of a credential leaf in a per-provider credentials tree. The leaf is `H(DOMAIN_CREDENTIAL, provider_id, submitter, type, attribute, expiry)`, which binds the credential to the submitter cryptographically; a forged credential leaf cannot be constructed without breaking Pedersen preimage resistance. However, the circuit does not verify a provider signature over the credential leaf or root in-circuit. Authority resolves to the registered publisher EOA, with rotation and revocation paths defined in [Validation Registries](#validation-registries). A compromised publisher EOA can publish a tree containing arbitrary `(submitter, attribute)` pairs until the owner rotates the publisher or revokes the root. Implementations whose threat model includes a compromised publisher key can layer an in-circuit signature scheme over the credential root or credential leaf; this is tracked as future work and is intentionally not required by this specification. -The signed variants (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) close this gap mathematically: their in-circuit Pedersen digest commits to (`chain_id`, `oracle_address`, `provider_set_hash`, `signals`, `weights`, `timestamp`, `submitter`), and the secp256k1 ECDSA verification of the provider's signature happens over that digest. Replaying a signed proof against a different chain or Oracle requires forging a new ECDSA signature over the new (chain_id, oracle_address) pair under the registered provider's key. Implementations MUST validate `chain_id == block.chainid` and `oracle_address == address(this)` on every signed-variant submission. +**Cross-chain replay.** The unsigned proof types (COMPLIANCE, RISK_SCORE, PATTERN, ATTESTATION, MEMBERSHIP, NON_MEMBERSHIP) do not include a chain identifier as a circuit public input. The same proof bytes may be replayed against the same Oracle on a different chain (or against an alternate Oracle deployment on the same chain), producing independent attestations on each. The on-chain proof-hash guard from [Proof Hash Computation](#proof-hash-computation) prevents replay-into-storage *within* a given (chain, Oracle) pair, but provides no in-circuit binding. The signed variants close this gap in-circuit: their Pedersen digest commits to `(chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`, and [Public Input Validation](#public-input-validation) requires `chain_id == block.chainid` and `oracle_address == address(this)` at the submission boundary. Replaying a signed proof against a different chain or Oracle requires forging a new ECDSA signature under the registered provider's key. Implementations whose threat model includes cross-deployment replay of unsigned proofs can fork the unsigned circuits to add `chain_id` and `oracle_address` as public inputs at the cost of a new verifier per chain. -**Verifier-layer reentrancy.** The `IUltraVerifier.verify(...)` interface MUST be declared `view` so that the EVM uses STATICCALL when invoking the verifier. STATICCALL prevents a malicious or compromised verifier from mutating state in the calling contract via reentrant calls. Implementations MUST NOT call verifiers via interfaces that omit the `view` modifier. +**Verifier-layer reentrancy.** A non-`view` verifier interface would expose the calling Oracle to reentrancy from a malicious or compromised verifier: the verifier could call back into the Oracle and mutate attestation state mid-verification. [Verifier Interface](#verifier-interface) requires the verifier function to be `view`, forcing the EVM to use `STATICCALL`, which prohibits state mutation in the callee. ## Copyright From 12300a736bbf30fa93403f165b4fa8a1b673f0ea Mon Sep 17 00:00:00 2001 From: DROOdotFOO Date: Wed, 3 Jun 2026 16:43:28 +0200 Subject: [PATCH 11/14] Add UAE (VARA) as 5th jurisdiction VARA's January 2026 anti-anonymity ruling carves out assets with "mitigating technologies" for traceability. ERC-8262 is such a mitigating technology, so adding UAE as a first-class jurisdiction lets the reference implementation route UAE- originating attestations through the same on-chain validation pipeline as the four pre-existing jurisdictions. UAE shares EU/UK's high-risk threshold (7100 bps / 71%) and adopts the strict-mode policy of US BSA and Singapore: requireSignedSignals=true, minMultiProviderThreshold=2. --- ERCS/erc-8262.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/ERCS/erc-8262.md b/ERCS/erc-8262.md index 7af017c70f7..d6f61b388c4 100644 --- a/ERCS/erc-8262.md +++ b/ERCS/erc-8262.md @@ -70,9 +70,9 @@ Notes on the proof type semantics: - **Risk Score (0x02).** Validators MUST reject trivially-true claims (`bound_lower = 0` for direction GT, `bound_lower >= MAX_RISK_SCORE_BPS` for direction LT, full-domain ranges). The `meetsThreshold` boolean stored on the attestation reflects only the cryptographic `result` field; integrators querying RISK_SCORE attestations should also verify the bounds match their integration's expectations. -- **Provider-signed variants (0x07 Compliance Signed, 0x08 Risk Score Signed).** Identical semantics to their unsigned siblings, plus an in-circuit secp256k1 ECDSA verification of a Pedersen digest committing to `(chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`. The provider's pubkey commitment is exposed as `signer_pubkey_hash`; implementations MUST validate it against an on-chain registry. The `chain_id` and `oracle_address` public inputs MUST match `block.chainid` and the consuming Oracle's address: this binds a single provider signature to one deployment so the same signed payload cannot mint attestations across chains or against alternate Oracle deployments. Strict-mode jurisdictions (e.g. US BSA, Singapore) reject the unsigned siblings entirely; permissive jurisdictions accept either form. +- **Provider-signed variants (0x07 Compliance Signed, 0x08 Risk Score Signed).** Identical semantics to their unsigned siblings, plus an in-circuit secp256k1 ECDSA verification of a Pedersen digest committing to `(chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`. The provider's pubkey commitment is exposed as `signer_pubkey_hash`; implementations MUST validate it against an on-chain registry. The `chain_id` and `oracle_address` public inputs MUST match `block.chainid` and the consuming Oracle's address: this binds a single provider signature to one deployment so the same signed payload cannot mint attestations across chains or against alternate Oracle deployments. Strict-mode jurisdictions (e.g. US BSA, Singapore, UAE VARA) reject the unsigned siblings entirely; permissive jurisdictions accept either form. -- **Compliance Multi-Signed (0x09).** Extends the signed model to M-of-N. The circuit bundles up to five parallel signer slots; a slot is active if its public `signer_pubkey_hash` is non-zero. Each active slot independently verifies a secp256k1 signature over a slot-specific Pedersen digest carrying its own `slot_index` (under a distinct `DOMAIN_MULTI_SIGNED_SIGNALS` tag) and independently asserts the per-provider risk score is below the jurisdiction high-risk floor. The Oracle MUST validate each non-zero slot's `signer_pubkey_hash` against the registry, MUST reject duplicate hashes across active slots, MUST enforce `chain_id == block.chainid` and `oracle_address == address(this)`, and MUST enforce `threshold_m >= JurisdictionConfig.minMultiProviderThreshold(jurisdictionId)` (e.g., US BSA and Singapore require M >= 2; permissive jurisdictions accept M >= 1). Forging an attestation under 0x09 requires compromising at least M of the N registered signing keys simultaneously. +- **Compliance Multi-Signed (0x09).** Extends the signed model to M-of-N. The circuit bundles up to five parallel signer slots; a slot is active if its public `signer_pubkey_hash` is non-zero. Each active slot independently verifies a secp256k1 signature over a slot-specific Pedersen digest carrying its own `slot_index` (under a distinct `DOMAIN_MULTI_SIGNED_SIGNALS` tag) and independently asserts the per-provider risk score is below the jurisdiction high-risk floor. The Oracle MUST validate each non-zero slot's `signer_pubkey_hash` against the registry, MUST reject duplicate hashes across active slots, MUST enforce `chain_id == block.chainid` and `oracle_address == address(this)`, and MUST enforce `threshold_m >= JurisdictionConfig.minMultiProviderThreshold(jurisdictionId)` (e.g., US BSA, Singapore, and UAE VARA require M >= 2; permissive jurisdictions accept M >= 1). Forging an attestation under 0x09 requires compromising at least M of the N registered signing keys simultaneously. ### Circuit Conventions @@ -86,7 +86,7 @@ The following structural constants are normative. Implementations that deviate p | `MAX_TRANSACTIONS` | 16 | transaction-slot count in PATTERN | | `MAX_WEIGHT` | 10000 | per-provider weight ceiling (overflow guard on the score) | -**Value ranges.** Each per-provider screening signal is a `u32` in `[0, 100]`. Each weight is a `u32` in `[0, MAX_WEIGHT]`. `weight_sum` is `u32`, strictly positive. `num_providers` is `u32` in `[1, MAX_PROVIDERS]`. `num_transactions` is `u32` in `[1, MAX_TRANSACTIONS]`. Jurisdiction IDs are `u8` in `[0, 3]` per Jurisdiction Configuration. In COMPLIANCE_MULTI_SIGNED (0x09), per-slot signal range is not enforced in-circuit because the per-slot signature already attests to the signed values; signers MUST sign only signals in `[0, 100]`. +**Value ranges.** Each per-provider screening signal is a `u32` in `[0, 100]`. Each weight is a `u32` in `[0, MAX_WEIGHT]`. `weight_sum` is `u32`, strictly positive. `num_providers` is `u32` in `[1, MAX_PROVIDERS]`. `num_transactions` is `u32` in `[1, MAX_TRANSACTIONS]`. Jurisdiction IDs are `u8` in `[0, 4]` per Jurisdiction Configuration. In COMPLIANCE_MULTI_SIGNED (0x09), per-slot signal range is not enforced in-circuit because the per-slot signature already attests to the signed values; signers MUST sign only signals in `[0, 100]`. **Public input encoding.** Every public input is a single field element of the proof system's scalar field (a 254-bit prime field for UltraHonk over BN254). Booleans encode as field `0` (false) or `1` (true). Ethereum addresses encode as the address packed into the low 160 bits of a field element. `u8`, `u32`, and `u64` values encode in the low bits with the high bits zero. Public-input arrays declared by the circuit's `main` (e.g., the five `signer_pubkey_hash` slots in 0x09) appear as one field element per array entry, in declared order. @@ -94,7 +94,7 @@ The following structural constants are normative. Implementations that deviate p **Domain-tag distinctness.** The reference implementation uses eight distinct domain tags, prepended to the Pedersen hash input array: one each for internal Merkle nodes, set-bound leaves, value leaves, subject-bound leaves, credential hashes, signed payload, multi-signed slot payload, and signer pubkey commitment. Three other commitments (provider set, config, transaction set) are fixed-arity Pedersen hashes over a single context and do not carry a separate domain tag; the input layout itself is unique to each context. Implementations MAY choose different field values for the eight tags but MUST keep them pairwise distinct and distinct from any field value reachable as a circuit input. -**Jurisdiction threshold lookup.** Define `highThreshold(jurisdictionId)` to return the high-risk threshold (in basis points) from the table in Jurisdiction Configuration: `EU=7100`, `US=6600`, `UK=7100`, `SG=7600`. +**Jurisdiction threshold lookup.** Define `highThreshold(jurisdictionId)` to return the high-risk threshold (in basis points) from the table in Jurisdiction Configuration: `EU=7100`, `US=6600`, `UK=7100`, `SG=7600`, `UAE=7100`. ### Commitment Layouts @@ -483,6 +483,7 @@ Implementations MUST publish jurisdiction thresholds openly. Risk scores are exp | 1 | US (BSA) | 0-2599 | 2600-6599 | >=6600 | | 2 | UK (MLR) | 0-3099 | 3100-7099 | >=7100 | | 3 | Singapore | 0-3599 | 3600-7599 | >=7600 | +| 4 | UAE (VARA) | 0-3099 | 3100-7099 | >=7100 | ### Attestation Lifecycle @@ -664,7 +665,7 @@ Implementations SHOULD publish, in deployment-facing documentation, the trust ti **Why attestation TTL?** Compliance status is not permanent. A user who was compliant yesterday may not be compliant today. Screening providers update their data continuously. The TTL forces periodic re-attestation while keeping the window configurable per deployment context. -**Why nine proof types?** Each proof type maps to a separate ZK circuit with distinct constraint logic. Compliance handles the core risk score check. Risk Score provides standalone threshold/range proofs. Pattern detects structuring behaviors. Attestation verifies credentials from authorized providers. Membership proves inclusion in an authorized set (whitelist). Non-membership proves exclusion from a sanctions list via sorted Merkle tree adjacency. The two single-signer `_signed` variants (Compliance Signed, Risk Score Signed) shadow their unsigned siblings but additionally verify one provider's secp256k1 ECDSA signature over the screening payload in-circuit and bind to (`chain_id`, `oracle_address`). The Compliance Multi-Signed variant (0x09) extends this further to M-of-N: up to five parallel signer slots, each independently signature- and floor-checked, with a runtime `threshold_m` and a per-jurisdiction floor for M. They are separate circuits rather than an oracle-side flag because the signature check materially changes the constraint set: an unsigned proof has no provenance for its `signals[]` private witness, while a signed proof cryptographically attests them. Strict-mode jurisdictions (US BSA, Singapore) accept only the signed forms. This separation keeps individual circuits small and auditable, and lets unsigned-tolerant jurisdictions deploy without paying the signature-verification gas overhead. +**Why nine proof types?** Each proof type maps to a separate ZK circuit with distinct constraint logic. Compliance handles the core risk score check. Risk Score provides standalone threshold/range proofs. Pattern detects structuring behaviors. Attestation verifies credentials from authorized providers. Membership proves inclusion in an authorized set (whitelist). Non-membership proves exclusion from a sanctions list via sorted Merkle tree adjacency. The two single-signer `_signed` variants (Compliance Signed, Risk Score Signed) shadow their unsigned siblings but additionally verify one provider's secp256k1 ECDSA signature over the screening payload in-circuit and bind to (`chain_id`, `oracle_address`). The Compliance Multi-Signed variant (0x09) extends this further to M-of-N: up to five parallel signer slots, each independently signature- and floor-checked, with a runtime `threshold_m` and a per-jurisdiction floor for M. They are separate circuits rather than an oracle-side flag because the signature check materially changes the constraint set: an unsigned proof has no provenance for its `signals[]` private witness, while a signed proof cryptographically attests them. Strict-mode jurisdictions (US BSA, Singapore, UAE VARA) accept only the signed forms. This separation keeps individual circuits small and auditable, and lets unsigned-tolerant jurisdictions deploy without paying the signature-verification gas overhead. ### What this standard does NOT prove @@ -886,7 +887,7 @@ Signed-variant gas (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) is dominated by in-cir **Emergency circuit break.** A discovered circuit or verifier vulnerability needs to be contained without redeploying. [Pause Mechanism](#pause-mechanism) mandates a pause that halts submission while keeping reads available, since proof-of-innocence depends on `getHistoricalProof` and `checkCompliance` staying live. Per-proof-type pause limits incident blast radius to the affected circuit. -**Trust model and signal honesty.** See [What this standard does NOT prove](#what-this-standard-does-not-prove) in Rationale for the full discussion. In short: the unsigned variants (COMPLIANCE, RISK_SCORE) are self-attested. The circuit verifies the score formula but not the screening signals themselves. The per-jurisdiction policy in [Public Input Validation](#public-input-validation) rejects the unsigned siblings for strict-mode jurisdictions (US BSA, Singapore in the reference implementation). Integrators in permissive jurisdictions that need signal honesty should require the signed variants, optionally composed with ATTESTATION proofs against an independently-published credential tree. [Trust Tier Disclosure](#trust-tier-disclosure) lets integrators read the deployment's accepted tier without inspecting on-chain state. +**Trust model and signal honesty.** See [What this standard does NOT prove](#what-this-standard-does-not-prove) in Rationale for the full discussion. In short: the unsigned variants (COMPLIANCE, RISK_SCORE) are self-attested. The circuit verifies the score formula but not the screening signals themselves. The per-jurisdiction policy in [Public Input Validation](#public-input-validation) rejects the unsigned siblings for strict-mode jurisdictions (US BSA, Singapore, and UAE VARA in the reference implementation). Integrators in permissive jurisdictions that need signal honesty should require the signed variants, optionally composed with ATTESTATION proofs against an independently-published credential tree. [Trust Tier Disclosure](#trust-tier-disclosure) lets integrators read the deployment's accepted tier without inspecting on-chain state. **ATTESTATION authority root.** ATTESTATION proofs verify Merkle inclusion of a credential leaf in a per-provider credentials tree. The leaf is `H(DOMAIN_CREDENTIAL, provider_id, submitter, type, attribute, expiry)`, which binds the credential to the submitter cryptographically; a forged credential leaf cannot be constructed without breaking Pedersen preimage resistance. However, the circuit does not verify a provider signature over the credential leaf or root in-circuit. Authority resolves to the registered publisher EOA, with rotation and revocation paths defined in [Validation Registries](#validation-registries). A compromised publisher EOA can publish a tree containing arbitrary `(submitter, attribute)` pairs until the owner rotates the publisher or revokes the root. Implementations whose threat model includes a compromised publisher key can layer an in-circuit signature scheme over the credential root or credential leaf; this is tracked as future work and is intentionally not required by this specification. From 4750669831257372aa95b5d194bd1082b24c2ffb Mon Sep 17 00:00:00 2001 From: DROOdotFOO Date: Thu, 4 Jun 2026 02:57:30 +0200 Subject: [PATCH 12/14] Qualify fixture paths to reference impl repo --- ERCS/erc-8262.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ERCS/erc-8262.md b/ERCS/erc-8262.md index d6f61b388c4..6efe080e648 100644 --- a/ERCS/erc-8262.md +++ b/ERCS/erc-8262.md @@ -709,7 +709,7 @@ This ERC introduces new interfaces and does not modify existing standards. It is ## Test Cases -The reference implementation includes binary proof fixtures in `test/fixtures/` for the six unsigned proof types. Static fixtures are not provided for the three signed variants (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED, COMPLIANCE_MULTI_SIGNED) because each requires a fresh secp256k1 ECDSA witness; those are exercised end-to-end in the TypeScript SDK consumer tests instead. Each unsigned fixture contains: +Binary proof fixtures for the six unsigned proof types are published in the reference implementation repository (see Reference Implementation below) under `test/fixtures/`. Static fixtures are not provided for the three signed variants (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED, COMPLIANCE_MULTI_SIGNED) because each requires a fresh secp256k1 ECDSA witness; those are exercised end-to-end in the TypeScript SDK consumer tests instead. Each unsigned fixture contains: - `proof`: the raw UltraHonk proof bytes (8640 bytes each) - `public_inputs`: the packed bytes32 public inputs @@ -723,7 +723,7 @@ The reference implementation includes binary proof fixtures in `test/fixtures/` | MEMBERSHIP | 160 bytes (5 inputs) | merkle_root, set_id, timestamp, is_member, submitter | | NON_MEMBERSHIP | 160 bytes (5 inputs) | merkle_root, set_id, timestamp, is_non_member, submitter | -All fixtures use Pedersen hash (Noir stdlib) for in-circuit commitments and Merkle tree construction. Fixtures can be regenerated via `scripts/generate-fixtures.sh`. +All fixtures use Pedersen hash (Noir stdlib) for in-circuit commitments and Merkle tree construction. Fixtures can be regenerated via `scripts/generate-fixtures.sh` in the reference implementation repository. ### Witness Annex @@ -831,11 +831,11 @@ submitter = "0xdead" # AND high_index == low_index + 1. ``` -Signed-variant witnesses (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) are identical to their unsigned siblings in the screening payload, plus `pubkey_x`, `pubkey_y`, `signature`, `signer_pubkey_hash`, `chain_id`, and `oracle_address`. The signature is computed off-chain by the provider over `H_pedersen(chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`. COMPLIANCE_MULTI_SIGNED (0x09) extends this to five parallel signer slots: each active slot supplies its own `(signals, weights, weight_sum, pubkey_x, pubkey_y, signature)` and a non-zero `signer_pubkey_hash`, where each signature commits to a slot-specific Pedersen digest under the `DOMAIN_MULTI_SIGNED_SIGNALS` tag (with embedded `slot_index`). Implementations producing fresh fixtures MUST sample fresh nonces — the reference implementation does this in `test/sdk/` rather than committing a static witness. +Signed-variant witnesses (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) are identical to their unsigned siblings in the screening payload, plus `pubkey_x`, `pubkey_y`, `signature`, `signer_pubkey_hash`, `chain_id`, and `oracle_address`. The signature is computed off-chain by the provider over `H_pedersen(chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`. COMPLIANCE_MULTI_SIGNED (0x09) extends this to five parallel signer slots: each active slot supplies its own `(signals, weights, weight_sum, pubkey_x, pubkey_y, signature)` and a non-zero `signer_pubkey_hash`, where each signature commits to a slot-specific Pedersen digest under the `DOMAIN_MULTI_SIGNED_SIGNALS` tag (with embedded `slot_index`). Implementations producing fresh fixtures MUST sample fresh nonces — the reference implementation does this in `test/sdk/` (in the reference implementation repository) rather than committing a static witness. ## Reference Implementation -A reference implementation accompanies this ERC. It consists of: +A reference implementation accompanies [ERC-8262](./erc-8262.md) and is published in a companion GitHub repository at github.com/xochi-fi/ERC-8262. All file paths in this section, in the Test Cases section, and in the Security Considerations section are relative to that repository. It consists of: - **Solidity contracts**: `src/ERC8262Verifier.sol`, `src/ERC8262Oracle.sol`, `src/SettlementRegistry.sol`, `src/Timelock.sol` (Foundry, Solidity 0.8.28, Cancun EVM) - **Noir circuits**: `circuits/` (one per proof type, pinned to nargo 1.0.0-beta.20 via `.tool-versions`) @@ -844,7 +844,7 @@ A reference implementation accompanies this ERC. It consists of: -Raw `bb`-generated UltraHonk verifiers exceed the [EIP-170](./eip-170.md) 24,576-byte runtime size limit for some of the nine proof types. The reference implementation rewrites the `pairing()` free function in inline Yul (`scripts/patch-pairing-yul.sh`), saving ~186 bytes per verifier (and ~800 gas per `verifyProof` call as a bonus) while staying byte-identical to the `bb`-generated semantics on the pairing precompile (`address(0x08)`) input layout. +Raw `bb`-generated UltraHonk verifiers exceed the [EIP-170](./eip-170.md) 24,576-byte runtime size limit for some of the nine proof types. The reference implementation rewrites the `pairing()` free function in inline Yul (via `scripts/patch-pairing-yul.sh`), saving ~186 bytes per verifier (and ~800 gas per `verifyProof` call as a bonus) while staying byte-identical to the `bb`-generated semantics on the pairing precompile (`address(0x08)`) input layout. ## Security Considerations From 684e6c476d52343b6534f9a06cb64487d0d36a7c Mon Sep 17 00:00:00 2001 From: DROOdotFOO Date: Thu, 4 Jun 2026 03:25:12 +0200 Subject: [PATCH 13/14] Fix self-link to use eip-8262.md slug for HTMLProofer --- ERCS/erc-8262.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERCS/erc-8262.md b/ERCS/erc-8262.md index 6efe080e648..1bcc5caee7d 100644 --- a/ERCS/erc-8262.md +++ b/ERCS/erc-8262.md @@ -835,7 +835,7 @@ Signed-variant witnesses (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) are identical to ## Reference Implementation -A reference implementation accompanies [ERC-8262](./erc-8262.md) and is published in a companion GitHub repository at github.com/xochi-fi/ERC-8262. All file paths in this section, in the Test Cases section, and in the Security Considerations section are relative to that repository. It consists of: +A reference implementation accompanies [ERC-8262](./eip-8262.md) and is published in a companion GitHub repository at github.com/xochi-fi/ERC-8262. All file paths in this section, in the Test Cases section, and in the Security Considerations section are relative to that repository. It consists of: - **Solidity contracts**: `src/ERC8262Verifier.sol`, `src/ERC8262Oracle.sol`, `src/SettlementRegistry.sol`, `src/Timelock.sol` (Foundry, Solidity 0.8.28, Cancun EVM) - **Noir circuits**: `circuits/` (one per proof type, pinned to nargo 1.0.0-beta.20 via `.tool-versions`) From 973a777bed1a46237b952c0d808514a25650bd34 Mon Sep 17 00:00:00 2001 From: DROO <65291057+DROOdotFOO@users.noreply.github.com> Date: Sun, 14 Jun 2026 19:24:51 +0200 Subject: [PATCH 14/14] ERC-8262: jurisdiction policy table + tighter settlement_root (#1) --- ERCS/erc-8262.md | 45 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/ERCS/erc-8262.md b/ERCS/erc-8262.md index 1bcc5caee7d..0f28071ba3d 100644 --- a/ERCS/erc-8262.md +++ b/ERCS/erc-8262.md @@ -66,13 +66,13 @@ Notes on the proof type semantics: - **Membership (0x05) and Non-membership (0x06).** The leaf is `leaf_hash_subject(value, set_id, salt)`. For membership, `value` is the submitter's address (the leaf is computed from the public `submitter` input + private `subject_salt`). For non-membership, `value` is the bracketing tree entry (`low_leaf` / `high_leaf`), and the proof asserts `low_leaf < submitter < high_leaf` using full-width Field comparison (no u64 ceiling). Tree publishers MUST sort leaves by `value`; the circuit additionally requires `high_index == low_index + 1` to prevent an attacker from skipping a real intermediate entry. -- **Pattern (0x03).** The `analysis_type` field selects the analysis kind: 1 = anti-structuring, 2 = velocity, 3 = round-amounts. Implementations that depend on a specific analysis (e.g., a settlement registry requiring anti-structuring) MUST verify the `analysis_type` field; storing only the `result` boolean is insufficient. The `settlement_root` public input is opaque to the circuit (set to 0 for standalone use, or to a downstream consumer's declarative binding value). Consumers that need to bind a pattern proof to a specific downstream state (e.g., the sub-settlements of a particular trade) MUST recompute the expected `settlement_root` from their own state and assert equality, and SHOULD mark each consumed pattern proof to prevent reuse across multiple bound contexts. +- **Pattern (0x03).** The `analysis_type` field selects the analysis kind: 1 = anti-structuring, 2 = velocity, 3 = round-amounts. Implementations that depend on a specific analysis (e.g., a settlement registry requiring anti-structuring) MUST verify the `analysis_type` field; storing only the `result` boolean is insufficient. The `settlement_root` public input is opaque to the circuit (set to 0 for standalone use, or to a downstream consumer's declarative binding value). Consumers that need to bind a pattern proof to a specific downstream state (e.g., the sub-settlements of a particular trade) MUST recompute the expected `settlement_root` from their own state and assert equality, and MUST mark each consumed pattern proof to prevent reuse across multiple bound contexts. The canonical computation for the sub-settlement use case is `keccak256(abi.encode(uint8 subTradeCount, bytes32[] subProofHashes)) mod BN254_FR_MODULUS`; the modular reduction fits the result into a BN254 scalar field element so it can be passed as a public input. Consumers SHOULD use this exact encoding. Off-by-one in field width or `abi.encode` byte layout produces a different root, and the proof's equality check rejects. - **Risk Score (0x02).** Validators MUST reject trivially-true claims (`bound_lower = 0` for direction GT, `bound_lower >= MAX_RISK_SCORE_BPS` for direction LT, full-domain ranges). The `meetsThreshold` boolean stored on the attestation reflects only the cryptographic `result` field; integrators querying RISK_SCORE attestations should also verify the bounds match their integration's expectations. -- **Provider-signed variants (0x07 Compliance Signed, 0x08 Risk Score Signed).** Identical semantics to their unsigned siblings, plus an in-circuit secp256k1 ECDSA verification of a Pedersen digest committing to `(chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`. The provider's pubkey commitment is exposed as `signer_pubkey_hash`; implementations MUST validate it against an on-chain registry. The `chain_id` and `oracle_address` public inputs MUST match `block.chainid` and the consuming Oracle's address: this binds a single provider signature to one deployment so the same signed payload cannot mint attestations across chains or against alternate Oracle deployments. Strict-mode jurisdictions (e.g. US BSA, Singapore, UAE VARA) reject the unsigned siblings entirely; permissive jurisdictions accept either form. +- **Provider-signed variants (0x07 Compliance Signed, 0x08 Risk Score Signed).** Identical semantics to their unsigned siblings, plus an in-circuit secp256k1 ECDSA verification of a Pedersen digest committing to `(chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`. The provider's pubkey commitment is exposed as `signer_pubkey_hash`; implementations MUST validate it against an on-chain registry. The `chain_id` and `oracle_address` public inputs MUST match `block.chainid` and the consuming Oracle's address: this binds a single provider signature to one deployment so the same signed payload cannot mint attestations across chains or against alternate Oracle deployments. Strict-mode jurisdictions (see Jurisdiction Policy) reject the unsigned siblings entirely; permissive jurisdictions accept either form. -- **Compliance Multi-Signed (0x09).** Extends the signed model to M-of-N. The circuit bundles up to five parallel signer slots; a slot is active if its public `signer_pubkey_hash` is non-zero. Each active slot independently verifies a secp256k1 signature over a slot-specific Pedersen digest carrying its own `slot_index` (under a distinct `DOMAIN_MULTI_SIGNED_SIGNALS` tag) and independently asserts the per-provider risk score is below the jurisdiction high-risk floor. The Oracle MUST validate each non-zero slot's `signer_pubkey_hash` against the registry, MUST reject duplicate hashes across active slots, MUST enforce `chain_id == block.chainid` and `oracle_address == address(this)`, and MUST enforce `threshold_m >= JurisdictionConfig.minMultiProviderThreshold(jurisdictionId)` (e.g., US BSA, Singapore, and UAE VARA require M >= 2; permissive jurisdictions accept M >= 1). Forging an attestation under 0x09 requires compromising at least M of the N registered signing keys simultaneously. +- **Compliance Multi-Signed (0x09).** Extends the signed model to M-of-N. The circuit bundles up to five parallel signer slots; a slot is active if its public `signer_pubkey_hash` is non-zero. Each active slot independently verifies a secp256k1 signature over a slot-specific Pedersen digest carrying its own `slot_index` (under a distinct `DOMAIN_MULTI_SIGNED_SIGNALS` tag) and independently asserts the per-provider risk score is below the jurisdiction high-risk floor. The Oracle MUST validate each non-zero slot's `signer_pubkey_hash` against the registry, MUST reject duplicate hashes across active slots, MUST enforce `chain_id == block.chainid` and `oracle_address == address(this)`, and MUST enforce `threshold_m >= JurisdictionConfig.minMultiProviderThreshold(jurisdictionId)` (see Jurisdiction Policy for per-jurisdiction minimums). Forging an attestation under 0x09 requires compromising at least M of the N registered signing keys simultaneously. ### Circuit Conventions @@ -479,11 +479,38 @@ Implementations MUST publish jurisdiction thresholds openly. Risk scores are exp | ID | Jurisdiction | Low (bps) | Medium (bps) | High / Filing trigger (bps) | | --- | ------------ | --------- | ------------ | --------------------------- | -| 0 | EU (AMLD6) | 0-3099 | 3100-7099 | >=7100 | -| 1 | US (BSA) | 0-2599 | 2600-6599 | >=6600 | -| 2 | UK (MLR) | 0-3099 | 3100-7099 | >=7100 | -| 3 | Singapore | 0-3599 | 3600-7599 | >=7600 | -| 4 | UAE (VARA) | 0-3099 | 3100-7099 | >=7100 | +| 0 | EU (AMLD6) | 0-3099 | 3100-7099 | >=7100 | +| 1 | US (BSA) | 0-2599 | 2600-6599 | >=6600 | +| 2 | UK (MLR) | 0-3099 | 3100-7099 | >=7100 | +| 3 | Singapore (MAS) | 0-3599 | 3600-7599 | >=7600 | +| 4 | UAE (VARA) | 0-3099 | 3100-7099 | >=7100 | + +### Jurisdiction Policy + +Implementations MUST publish two per-jurisdiction policy values alongside the +threshold table: whether unsigned screening proofs (COMPLIANCE 0x01, RISK_SCORE +0x02) are accepted, and the minimum `threshold_m` for COMPLIANCE_MULTI_SIGNED +(0x09). The reference values are: + +| ID | Jurisdiction | Accepts unsigned (0x01, 0x02) | Min `threshold_m` (0x09) | +| --- | --------------- | ----------------------------- | ------------------------ | +| 0 | EU (AMLD6) | yes | 1 | +| 1 | US (BSA) | no | 2 | +| 2 | UK (MLR) | yes | 1 | +| 3 | Singapore (MAS) | no | 2 | +| 4 | UAE (VARA) | no | 2 | + +Compliant implementations MUST reject submissions of unsigned screening proofs +for any jurisdiction whose "Accepts unsigned" column is `no`, and MUST reject +COMPLIANCE_MULTI_SIGNED submissions whose `threshold_m` is below the per-jurisdiction +minimum. The reference enforces both via `JurisdictionConfig.requireSignedSignals(uint8)` +and `JurisdictionConfig.minMultiProviderThreshold(uint8)`. + +Implementations targeting a jurisdiction not enumerated above SHOULD use +signed-only + M >= 2 if the regulator requires provider attestation, and the +EU defaults (accepts unsigned + M = 1) otherwise. The policy MUST be +hard-coded or governance-mutable under the same time-delay guarantees as +verifier upgrades; it MUST NOT be settable per-submission. ### Attestation Lifecycle @@ -665,7 +692,7 @@ Implementations SHOULD publish, in deployment-facing documentation, the trust ti **Why attestation TTL?** Compliance status is not permanent. A user who was compliant yesterday may not be compliant today. Screening providers update their data continuously. The TTL forces periodic re-attestation while keeping the window configurable per deployment context. -**Why nine proof types?** Each proof type maps to a separate ZK circuit with distinct constraint logic. Compliance handles the core risk score check. Risk Score provides standalone threshold/range proofs. Pattern detects structuring behaviors. Attestation verifies credentials from authorized providers. Membership proves inclusion in an authorized set (whitelist). Non-membership proves exclusion from a sanctions list via sorted Merkle tree adjacency. The two single-signer `_signed` variants (Compliance Signed, Risk Score Signed) shadow their unsigned siblings but additionally verify one provider's secp256k1 ECDSA signature over the screening payload in-circuit and bind to (`chain_id`, `oracle_address`). The Compliance Multi-Signed variant (0x09) extends this further to M-of-N: up to five parallel signer slots, each independently signature- and floor-checked, with a runtime `threshold_m` and a per-jurisdiction floor for M. They are separate circuits rather than an oracle-side flag because the signature check materially changes the constraint set: an unsigned proof has no provenance for its `signals[]` private witness, while a signed proof cryptographically attests them. Strict-mode jurisdictions (US BSA, Singapore, UAE VARA) accept only the signed forms. This separation keeps individual circuits small and auditable, and lets unsigned-tolerant jurisdictions deploy without paying the signature-verification gas overhead. +**Why nine proof types?** Each proof type maps to a separate ZK circuit with distinct constraint logic. Compliance handles the core risk score check. Risk Score provides standalone threshold/range proofs. Pattern detects structuring behaviors. Attestation verifies credentials from authorized providers. Membership proves inclusion in an authorized set (whitelist). Non-membership proves exclusion from a sanctions list via sorted Merkle tree adjacency. The two single-signer `_signed` variants (Compliance Signed, Risk Score Signed) shadow their unsigned siblings but additionally verify one provider's secp256k1 ECDSA signature over the screening payload in-circuit and bind to (`chain_id`, `oracle_address`). The Compliance Multi-Signed variant (0x09) extends this further to M-of-N: up to five parallel signer slots, each independently signature- and floor-checked, with a runtime `threshold_m` and a per-jurisdiction floor for M. They are separate circuits rather than an oracle-side flag because the signature check materially changes the constraint set: an unsigned proof has no provenance for its `signals[]` private witness, while a signed proof cryptographically attests them. Strict-mode jurisdictions (see Jurisdiction Policy) accept only the signed forms. This separation keeps individual circuits small and auditable, and lets unsigned-tolerant jurisdictions deploy without paying the signature-verification gas overhead. ### What this standard does NOT prove