---
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), Jan (@sssngth)
discussions-to: https://ethereum-magicians.org/t/erc-8262-zero-knowledge-compliance-oracle/28543
status: Review
type: Standards Track
category: ERC
created: 2026-04-07
requires: 165
---

## Abstract

A standard interface for on-chain verification of regulatory compliance (anti-money laundering (AML), sanctions screening, anti-structuring) using zero-knowledge (ZK) 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 choice between transparency and privacy. Transparent execution exposes trades to maximal extractable value (MEV) extraction. Privacy tools have been sanctioned for lacking compliance mechanisms.

Existing approaches fall short:

- View keys: reveal raw transaction data to auditors on request. This is delayed transparency.
- TEE-based compliance: relies on hardware trust assumptions broken by side-channel attacks and key extraction.
- Compliance-by-exclusion (privacy pools): proves absence from a bad set, not compliance with jurisdiction rules.

This Ethereum Request for Comment (ERC) proves compliance cryptographically at transaction time. The proof commits to screening results, jurisdiction thresholds, and provider attestations. Regulators verify the proof without seeing the underlying data.

## Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174).

### 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 time-to-live (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 (e.g., Groth16, PLONK, UltraHonk). The [reference implementation](../assets/eip-8262/README.md) uses UltraHonk with Noir circuits and Barretenberg-generated verifiers; its README lists pinned tool versions and licenses.

### Proof Types

Implementations MUST support the following proof types. Each type is a separate ZK circuit with its own verification key.

Every proof type includes `submitter` as a public input. Checks on all public inputs are in [Public Input Validation](#public-input-validation).

| 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`, `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`, `timestamp`, `signer_pubkey_hash`, `chain_id`, `oracle_address`, `submitter` | `signals`, `weights`, `weight_sum`, `provider_ids`, `num_providers`, `signature`, `pubkey_x`, `pubkey_y`               |
| 0x09 | Compliance Multi-Signed | `compliance_multi_signed` | `jurisdiction_id`, `provider_set_hash`, `config_hash`, `timestamp`, `meets_threshold`, `threshold_m`, `signer_pubkey_hash_i`, `chain_id`, `oracle_address`, `submitter`             | (per-signer-slot; 5 parallel slots per proof): `signals`, `weights`, `weight_sum`, `pubkey_x`, `pubkey_y`, `signature` |

- **Compliance (0x01).** Computes a weighted risk score from up to `MAX_PROVIDERS` provider signals; `meets_threshold` is `true` when the score is strictly below `highThreshold(jurisdictionId)`. `config_hash` and `provider_set_hash` commit to the weight configuration and provider set; the oracle validates both against its registries. Strict-mode jurisdictions ([Jurisdiction Thresholds & Policies](#jurisdiction-thresholds--policies)) reject 0x01 and require 0x07 or 0x09.

- **Risk Score (0x02).** The stored `meetsThreshold` reflects only `result`; integrators should also check that the bounds match their expectations.

- **Pattern (0x03).** `analysis_type`: `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 `analysis_type`; storing only `result` is insufficient. `settlement_root` is opaque to the circuit (`0` for stand-alone use, else a downstream consumer's binding value). Consumers that need to bind a pattern proof to a specific downstream state (e.g., the sub-settlements of a 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 sub-settlement computation is `keccak256(abi.encode(uint8 subTradeCount, bytes32[] subProofHashes)) mod BN254_FR_MODULUS`, reduced to fit a BN254 scalar; consumers SHOULD use this exact encoding.

- **Attestation (0x04).** The leaf is `leaf_hash_value(credential_hash)` with `credential_hash` per `H_credential` ([Commitment Layouts](#commitment-layouts)), binding the credential to `submitter` at issuance. `credential_root` references a per-provider tree registered via `publishCredentialRoot`; the root's on-chain `providerId` must match the proof's `provider_id`.

- **Membership (0x05) and Non-Membership (0x06).** The leaf is `leaf_hash_subject(value, set_id, salt)`. For 0x05, `value` is the public `submitter` (with private `subject_salt`). For 0x06, `value` is the bracketing entry (`low_leaf` / `high_leaf`); the proof asserts `low_leaf < submitter < high_leaf` by full-width field comparison and `high_index == low_index + 1`. Leaf ordering is specified in [Non-Membership Proof Security](#non-membership-proof-security).

- **Compliance Signed (0x07) and Risk Score Signed (0x08).** Unsigned-sibling semantics plus in-circuit secp256k1 ECDSA verification of `H_signed` ([Commitment Layouts](#commitment-layouts)) over `(domain, chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`. Each variant has its own `domain` tag, so a `signature` for one cannot satisfy the other. Both expose the signed `timestamp`; the oracle applies freshness and the ratchet to it ([Public Input Validation](#public-input-validation)), bounding how long a `signature` can mint attestations. Implementations MUST validate `signer_pubkey_hash` against an on-chain registry. `chain_id` and `oracle_address` MUST match `block.chainid` and the consuming oracle's address, binding a `signature` to one deployment. Strict-mode jurisdictions reject the unsigned siblings; permissive jurisdictions accept either form.

- **Compliance Multi-Signed (0x09).** M-of-N extension of 0x07 with five signer slots; a slot is active if its public `signer_pubkey_hash_i` is non-zero. Each active slot verifies a secp256k1 signature over its own `H_multi_slot` digest (carrying `slot_index`, under `DOMAIN_MULTI_SIGNED_SIGNALS`) and asserts its per-provider risk score is below the jurisdiction high-risk floor. The oracle MUST validate each non-zero slot's `signer_pubkey_hash_i` 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)` ([Jurisdiction Thresholds & Policies](#jurisdiction-thresholds--policies)). Forgery requires compromising M registered signing keys.

### Circuit Conventions

These constants are normative. Deviating implementations produce incompatible verifiers and cannot share registries.

| Constant              | Value   | Applies to                                                                                                        |
| --------------------- | ------- | ----------------------------------------------------------------------------------------------------------------- |
| `MAX_WEIGHT`          | `10000` | per-provider weight ceiling (overflow guard on the score)                                                         |
| `MAX_PROVIDERS`       | `8`     | provider-slot count in Compliance (0x01), Risk Score (0x02), Compliance Signed (0x07), Risk Score Signed (0x08)   |
| `MAX_TRANSACTIONS`    | `16`    | transaction-slot count in Pattern (0x03)                                                                          |
| `MERKLE_DEPTH`        | `20`    | tree depth for Attestation (0x04), Membership (0x05), Non-Membership (0x06)                                       |
| `MAX_PROVIDERS_MULTI` | `5`     | signer-slot count in Compliance Multi-Signed (0x09)                                                               |

**Value ranges.** Each of the `signals` is a `u32` in `[0, 100]`. Each of the `weights` is a `u32` in `[0, MAX_WEIGHT]`. `weight_sum` is a `u32`, `> 0`. `num_providers` is a `u32` in `[1, MAX_PROVIDERS]`. `num_transactions` is a `u32` in `[1, MAX_TRANSACTIONS]`. `jurisdiction_id` is a `u8` in `[0, 4]` ([Jurisdiction Thresholds & Policies](#jurisdiction-thresholds--policies)). In Compliance Multi-Signed (0x09) the per-slot `signals` range is not enforced in-circuit, since the slot `signature` attests the values; signers MUST sign only `signals` in `[0, 100]`.

**Public input encoding.** Every public input is one element of the proof system's scalar field (254-bit for UltraHonk over BN254). Booleans encode as `0` or `1`. Addresses occupy the low 160 bits. `u8`, `u32`, and `u64` values occupy the low bits with high bits zero. Public-input arrays declared by `main()` (e.g., the five `signer_pubkey_hash_i` of 0x09) take one field element per 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, so inactive slots add nothing to commitment hashes. In Compliance Multi-Signed (0x09), a signer slot is active if its public `signer_pubkey_hash_i` is non-zero; inactive slots carry no signature but still satisfy constraint 2 of Compliance Multi-Signed (0x09).

**Domain tags.** Every tagged Pedersen commitment prepends one of these field constants to its input array. The values are normative: signer registrations and provider signatures are computed against them. The nine values MUST be pairwise distinct. The provider-set, config, and transaction-set commitments are untagged fixed-arity hashes whose layouts differ from every tagged layout.

| Constant                      | Value                | ASCII      | Used by                                                      |
| ----------------------------- | -------------------- | ---------- | ------------------------------------------------------------ |
| `DOMAIN_INTERNAL`             | `0x4d4b4c5f494e54`   | `MKL_INT`  | Merkle internal nodes                                        |
| `DOMAIN_LEAF_SET`             | `0x4d4b4c5f4c5354`   | `MKL_LST`  | `leaf_hash_set`                                              |
| `DOMAIN_LEAF_VALUE`           | `0x4d4b4c5f56414c`   | `MKL_VAL`  | `leaf_hash_value`                                            |
| `DOMAIN_LEAF_SUBJECT`         | `0x4d4b4c5f535542`   | `MKL_SUB`  | `leaf_hash_subject`                                          |
| `DOMAIN_CREDENTIAL`           | `0x435245445f48`     | `CRED_H`   | `H_credential`                                               |
| `DOMAIN_SIGNED_SIGNALS`       | `0x5349475f53494753` | `SIG_SIGS` | `H_signed` for Compliance Signed (0x07)                      |
| `DOMAIN_RISK_SIGNED_SIGNALS`  | `0x52534b5f53494753` | `RSK_SIGS` | `H_signed` for Risk Score Signed (0x08)                      |
| `DOMAIN_MULTI_SIGNED_SIGNALS` | `0x4d554c54495f5349` | `MULTI_SI` | `H_multi_slot` for Compliance Multi-Signed (0x09)            |
| `DOMAIN_SIGNER_PUBKEY`        | `0x5349475f504b`     | `SIG_PK`   | `H_signer_pubkey`                                            |

**Jurisdiction threshold lookup.** Define `highThreshold(jurisdictionId)` to return the high-risk threshold, in basis points (bps), from the first table in [Jurisdiction Thresholds & Policies](#jurisdiction-thresholds--policies).

### Commitment Layouts

All commitments use the same Pedersen hash over the proof system's scalar field. Fields are listed left to right.

- **`H_provider_set(provider_ids[N], weights[N])`** with `N = MAX_PROVIDERS`. Input is a 16-entry array: `[provider_ids[0], weights[0], provider_ids[1], weights[1], ..., provider_ids[7], weights[7]]`. Inactive provider slots contribute `(0, 0)` pairs.
- **`H_config(weights[N])`** with `N = MAX_PROVIDERS`. 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 transaction 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(domain, chain_id, oracle_address, provider_set_hash, signals[8], weights[8], timestamp, submitter)`**. Used by Compliance Signed (0x07) with `domain = DOMAIN_SIGNED_SIGNALS` and by Risk Score Signed (0x08) with `domain = DOMAIN_RISK_SIGNED_SIGNALS`. Input is a 22-entry array: `[domain, chain_id, oracle_address, provider_set_hash, signals[0..7], weights[0..7], timestamp, submitter]`. The provider's secp256k1 ECDSA `signature` is over the 32-byte big-endian serialization of this hash. Every signed proof type MUST use its own signing domain tag ([Circuit Conventions](#circuit-conventions)); the tag binds a `signature` to one proof type.
- **`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..7], weights[0..7], timestamp, submitter]`. Each active slot's `signature` is over this digest with its own `slot_index`. The distinct tag stops a 0x07 `signature` from satisfying a 0x09 slot.
- **`H_signer_pubkey(pubkey_x, pubkey_y)`**. Commits to a secp256k1 signing key. Each 32-byte coordinate splits into high and low 16-byte halves, since the BN254 scalar field cannot hold an arbitrary 256-bit integer. Input is `[DOMAIN_SIGNER_PUBKEY, x_hi, x_lo, y_hi, y_lo]` (5 fields). Administrators register the result via `registerSignerPubkeyHash`.

### Per-Type Circuit Specifications

Each circuit's `main()` declares the inputs below and enforces the listed constraints. Constraint numbers are normative. Cross-cutting requirements (active-slot invariants, timestamp bounds, `submitter != 0`) are in [Circuit Conventions](#circuit-conventions) and [Circuit Constraints](#circuit-constraints).

#### Compliance (0x01)

Public inputs (6, in order): `jurisdiction_id` (`u8` in `[0, 4]`), `provider_set_hash` (`Field`), `config_hash` (`Field`), `timestamp` (`u64`), `meets_threshold` (`bool`), `submitter` (`uint160`, packed).

Private inputs: `signals[8]` (`u32` each in `[0, 100]`), `weights[8]` (`u32` each in `[0, MAX_WEIGHT]`), `weight_sum` (`u32`, `> 0`), `provider_ids[8]` (`Field`), `num_providers` (`u32` in `[1, MAX_PROVIDERS]`).

Constraints:

1. Active-slot invariant, per [Circuit Conventions](#circuit-conventions).
2. `weight_sum == sum(weights[0..7])` (sum over the full array; inactive provider 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..7} 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` in `[1, 2]`, where `1 = threshold` and `2 = range`), `direction` (`u8` in `[1, 2]`, where `1 = greater than (GT)` and `2 = less than (LT)`; ignored for `range` proofs), `bound_lower` (`u32`, in bps), `bound_upper` (`u32`, in bps; `0` for `threshold` proofs), `result` (`bool`), `config_hash` (`Field`), `provider_set_hash` (`Field`), `submitter` (`uint160`, packed).

Private inputs: same as Compliance (0x01).

Constraints: (1-5) identical to Compliance (0x01), 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 MUST also reject trivially-true or unsatisfiable bounds (trivial-bound rule under RISK_SCORE (0x02) in [Public Input Validation](#public-input-validation)).

#### Pattern (0x03)

Public inputs (7, in order): `analysis_type` (`u8` in `[1, 3]`, where `1 = anti-structuring`, `2 = velocity`, and `3 = round-amounts`), `result` (`bool`, where `true = clean` and `false = structuring detected`), `reporting_threshold` (`u64`), `time_window` (`u64`), `tx_set_hash` (`Field`), `submitter` (`uint160`, packed), `settlement_root` (`Field`).

Private inputs: `amounts[16]` (`u64`), `timestamps[16]` (`u64`), `num_transactions` (`u32` in `[1, MAX_TRANSACTIONS]`).

Constraints:

1. Active-slot invariant, per [Circuit Conventions](#circuit-conventions) (inactive transaction 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`: predicate over `amounts` and `reporting_threshold`.
   - `analysis_type == 2`: predicate over `timestamps`, `num_transactions`, and `time_window`.
   - `analysis_type == 3`: 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` (`Field`), `credential_type` (`u8` in `[1, 4]`, where `1 = KYC basic`, `4 = institutional`, `2` and `3` are reserved), `is_valid` (`bool`), `credential_root` (`Field`), `current_timestamp` (`u64`), `submitter` (`uint160`, packed).

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` (`Field`), `set_id` (`Field`), `timestamp` (`u64`), `is_member` (`bool`), `submitter` (`uint160`, packed).

Private inputs: `subject_salt` (`Field`; `0` for public sets), `merkle_index` (`Field`), `merkle_path[MERKLE_DEPTH]` (`Field`).

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` (`Field`), `set_id` (`Field`), `timestamp` (`u64`), `is_non_member` (`bool`), `submitter` (`uint160`, packed).

Private inputs: `low_leaf` (`Field`), `low_leaf_salt` (`Field`), `low_index` (`Field`), `low_path[MERKLE_DEPTH]` (`Field`), `high_leaf` (`Field`), `high_leaf_salt` (`Field`), `high_index` (`Field`), `high_path[MERKLE_DEPTH]` (`Field`).

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): `jurisdiction_id`, `provider_set_hash`, `config_hash`, `timestamp`, `meets_threshold` (the first five Compliance (0x01) inputs, same types), then `signer_pubkey_hash` (`Field`), `chain_id` (`Field`), `oracle_address` (`Field`), `submitter` (`uint160`, packed).

Private inputs: the Compliance (0x01) private inputs, plus `signature` (`u8[64]`, secp256k1 ECDSA in raw `r || s` format), `pubkey_x[32]` (`u8`), `pubkey_y[32]` (`u8`).

Constraints: all Compliance (0x01) constraints (1-7), plus:

8. `H_signer_pubkey(pubkey_x, pubkey_y) == signer_pubkey_hash`.
9. `digest = H_signed(DOMAIN_SIGNED_SIGNALS, 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`, `oracle_address == address(this)`, and `timestamp` freshness, per [Public Input Validation](#public-input-validation).

#### Risk Score Signed (0x08)

Public inputs (12, in order): `proof_type` (`u8`), `direction` (`u8`), `bound_lower` (`u32`), `bound_upper` (`u32`), `result` (`bool`), `config_hash` (`Field`), `provider_set_hash` (`Field`), `timestamp` (`Field`), `signer_pubkey_hash` (`Field`), `chain_id` (`Field`), `oracle_address` (`Field`), `submitter` (`uint160`, packed). The first seven match Risk Score (0x02). `timestamp` is the provider's signing time.

Private inputs: the Risk Score (0x02) private inputs, plus `signature` (`u8[64]`, secp256k1 ECDSA in raw `r || s` format), `pubkey_x[32]` (`u8`), `pubkey_y[32]` (`u8`) as in Compliance Signed (0x07).

Constraints: Risk Score (0x02) constraints (1-9), plus:

10. `timestamp` satisfies [Circuit Constraints](#circuit-constraints).
11. `H_signer_pubkey(pubkey_x, pubkey_y) == signer_pubkey_hash`.
12. `digest = H_signed(DOMAIN_RISK_SIGNED_SIGNALS, chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)`.
13. `ecdsa_secp256k1_verify(pubkey_x, pubkey_y, signature, digest_be_bytes) == true`.

The oracle also validates `signer_pubkey_hash` is registered, `chain_id == block.chainid`, `oracle_address == address(this)`, and `timestamp` freshness, per [Public Input Validation](#public-input-validation).

#### Compliance Multi-Signed (0x09)

Public inputs (14, in order): `jurisdiction_id` (`u8` in `[0, 4]`), `provider_set_hash` (`Field`), `config_hash` (`Field`), `timestamp` (`u64`), `meets_threshold` (`bool`), `threshold_m` (`u8`), `signer_pubkey_hash_i` (where `i = 0, ..., MAX_PROVIDERS_MULTI - 1`), `chain_id` (`Field`), `oracle_address` (`Field`), `submitter` (`uint160`, packed).

Private inputs: per-slot arrays of size 5, each with `signals[8]` (`u32` each in `[0, 100]`), `weights[8]` (`u32` each `<= MAX_WEIGHT`), `weight_sum` (`u32` `> 0`), `pubkey_x[32]` (`u8`), `pubkey_y[32]` (`u8`), `signature` (`u8[64]`; secp256k1 ECDSA in raw `r || s` format). Inactive signer 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`:

1. Let `active_i = (signer_pubkey_hash_i != 0)`.
2. `weight_sum_i > 0` and `weight_sum_i == sum_j weights_i[j]`, for all slots, so 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. Only constraint 5 is gated by `active_i`. On inactive slots `pubkey_x_i`, `pubkey_y_i` and `signature_i` are unconstrained; constraints 2-4 apply to every slot.

Cross-slot:

7. `count(active_i for i in 0..MAX_PROVIDERS_MULTI - 1) >= 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` (field `1`). The step 5 checks are hard asserts, so no proof exists with `meets_threshold = false`; the field exists for layout parity with Compliance Signed (0x07) and oracle routing.
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 verification to per-proof-type verifier contracts generated by the ZK backend (e.g., `bb write_solidity_verifier` for UltraHonk; see [Reference Implementation](#reference-implementation)).

```solidity
interface IERC8262Verifier {
    /// @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(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 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 oracle and mutate attestation state.

### Batch Verification Limits

Implementations MUST enforce a maximum batch size for `verifyProofBatch` and `submitComplianceBatch`. The cap MUST be chosen so a full batch fits within the target chain's block gas limit with headroom for submission overhead (registry lookups, replay-guard SSTORE, event emission). The reference caps both at 10; a full 10-entry `submitComplianceBatch` costs about 24.1 M gas, within a 30 M-gas block. Deployments on chains with larger or smaller block budgets MUST recalibrate.

### Oracle Interface

```solidity
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, 4=UAE)
        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 (0x01/0x07/0x09 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(uint8 indexed proofType, bytes32 indexed merkleRoot);
    event MerkleRootRevoked(uint8 indexed proofType, 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, 4=UAE)
    /// @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, 4=UAE)
    /// @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
    /// @dev Reads the jurisdiction's compliance slot, which only COMPLIANCE_MULTI_SIGNED
    ///      and, where the multi-provider floor is 1, COMPLIANCE / COMPLIANCE_SIGNED write.
    /// @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 the latest attestation of one proof type
    /// @dev Every accepted proof writes its own (subject, jurisdiction, proofType) slot.
    /// @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(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)))
```

`proofType` scopes uniqueness per proof type. `block.chainid` and `address(this)` prevent on-chain replay across forks or oracle deployments. In-circuit chain binding exists only in the signed variants ([Public Input Validation](#public-input-validation)).

### Jurisdiction Thresholds & Policies

Values below are current at publication.

Implementations MUST publish jurisdiction thresholds openly. Risk scores are expressed in bps (`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 (MAS) | `0-3599`  | `3600-7599`  | `>=7600`                    |
| 4  | UAE (VARA)      | `0-3099`  | `3100-7099`  | `>=7100`                    |

Implementations MUST publish two per-jurisdiction policy values alongside the thresholds: whether unsigned screening proofs (Compliance (0x01), Risk Score (0x02)) are accepted, and the minimum `threshold_m` for Compliance Multi-Signed (0x09). Reference values:

| 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 unsigned screening proofs for any jurisdiction whose "Accepts unsigned" column is `no`, and MUST reject Compliance Multi Signed (0x09) 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 Compliance Multi Signed (0x09)-only with `M >= 2` if the regulator requires provider attestation, and the EU defaults (accepts Compliance (0x01) with `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

Attestation TTL:

- Default TTL: 86400 seconds (24 hours)
- Minimum TTL: 3600 seconds (1 hour)
- Maximum TTL: 2592000 seconds (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 (POI). The oracle administrator updates the TTL via `updateAttestationTTL()`.

### Provider Weight Publication

Implementations SHOULD publish provider `weights` as an on-chain configuration hash. Weight changes MUST emit `ProviderWeightsUpdated` with the new `configHash`, `timestamp`, and an optional `metadataURI` pointing to the full configuration (e.g., on IPFS or Arweave).

Provider configuration MUST be versioned. Implementations SHOULD keep a history of configuration hashes, recording which weights were active when a proof was generated. Implementations SHOULD support revoking historical configuration hashes found 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 (e.g., 256 entries, first in first out (FIFO) eviction of the oldest non-current entries). An evicted configuration MUST stop being accepted, and registering a configuration hash that is already registered (current or historical) MUST revert.

### Proof Type Routing

Implementations MUST maintain a registry mapping each proof type to a per-circuit verifier contract, each with its own verification key. The main verifier contract routes:

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; see [Administrative Operations](#administrative-operations).

### Verifier Versioning

Verifier upgrades MUST NOT invalidate proofs that were valid under a prior verifier. `verifierUsed` alone is not enough: a counterparty may need to re-run verification months later, e.g., to recompute the POI after a circuit bug or to audit a historical attestation.

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 found 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; instead, propose a replacement through the upgrade timelock, then revoke the prior version.

Revocation MAY have two paths: a delayed default path and an immediate path gated behind the `guardian` role, for locking in revocation of a paused proof type before the timelock elapses. The reference delays the routine path 6 h.

Revoking a version MUST also withdraw the attestations it verified: `checkCompliance` and `checkComplianceByType` MUST NOT report as valid an attestation whose `verifierUsed` belongs to a revoked version. Implementations MUST NOT revoke an older version whose address is the current verifier, and MUST NOT reinstall a revoked verifier address. Revoking a signer key, configuration, or root blocks new submissions only; implementations SHOULD give the `guardian` role a way to invalidate individual attestations already issued under a compromised one (the reference implementation provides `invalidateAttestation(proofHash)` and `isAttestationValid(proofHash)`).

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.

### Public Input Validation

Implementations MUST validate public inputs semantically for each proof type before forwarding to the per-circuit verifier. The circuit guarantees internal consistency; the oracle MUST verify that the committed inputs match the expected context (e.g., a known config hash, a registered merkle root), or a proof generated for one context can be replayed in another.

Public inputs MUST be 32-byte aligned. Implementations MUST reject `publicInputs` where `length % 32 != 0`.

These checks apply at the submission boundary, in addition to the in-circuit constraints in [Per-Type Circuit Specifications](#per-type-circuit-specifications). A proof that satisfies its circuit but fails these checks MUST be rejected.

Each per-type table lists every public input at its circuit index. `--` marks an input the Oracle deliberately does not check; the row states why.

#### Cross-cutting rules

These apply wherever the named input is present.

| Rule                     | Applies to                                     | Requirement                                                                                                                                                                          |
| ------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Submitter binding**    | all proof types                                | `submitter` MUST equal `msg.sender`. See [Submitter Binding](#submitter-binding).                                                                                                    |
| **Positive result**      | all proof types                                | The boolean result field MUST be exactly `bytes32(uint256(1))`. See [Proof Result Validation](#proof-result-validation).                                                             |
| **Timestamp freshness**  | 0x01, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09       | The timestamp is prover-chosen and only loosely bounded in-circuit ([Circuit Constraints](#circuit-constraints)). It MUST NOT be in the future and MUST be within an implementation-defined maximum age (reference: 1 hour). For 0x07 and 0x08 it is the provider's signing time, committed in `H_signed`, so this rule also expires the provider `signature`. |
| **Timestamp ratchet**    | all proof types                                | Implementations MUST maintain a non-decreasing timestamp per `(subject, jurisdiction, proofType)` and reject submissions below it, so a stale proof cannot overwrite a newer attestation of the same type. Implementations MUST also maintain one per `(subject, jurisdiction)` for the compliance slot, advanced only by proofs that write that slot. Equal values MUST remain acceptable. Types without a timestamp input ratchet on `block.timestamp` in their per-type slot only. |
| **Compliance slot**      | 0x01, 0x07, 0x09                               | `checkCompliance` MUST reflect only proofs that satisfy the jurisdiction's full policy: Compliance Multi-Signed (0x09), and Compliance (0x01) or Compliance Signed (0x07) only where the jurisdiction's `minMultiProviderThreshold` is 1. Every other accepted proof, including Risk Score variants, MUST be recorded only in its per-type slot, readable via `checkComplianceByType`. |
| **Jurisdiction policy**  | 0x01, 0x02 (rejection), 0x09 (`threshold_m`)   | Unsigned screening proofs MUST be rejected for jurisdictions requiring signed signals. See [Jurisdiction Thresholds & Policies](#jurisdiction-thresholds--policies).                                              |

#### COMPLIANCE (0x01)

| #   | Public input        | Oracle validation                                                                                                             |
| --- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| 0   | `jurisdiction_id`   | MUST equal the `jurisdictionId` argument of `submitCompliance()`.                                                             |
| 1   | `provider_set_hash` | MUST equal the `providerSetHash` argument of `submitCompliance()`.                                                            |
| 2   | `config_hash`       | MUST be registered and non-revoked in the config hash registry, and MUST NOT expand to any denied provider.                   |
| 3   | `timestamp`         | Timestamp freshness; timestamp ratchet.                                                                                       |
| 4   | `meets_threshold`   | Positive result.                                                                                                              |
| 5   | `submitter`         | Submitter binding.                                                                                                            |

#### RISK_SCORE (0x02)

| #   | Public input        | Oracle validation                                                                                                             |
| --- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| 0   | `proof_type`        | MUST be 1 (threshold) or 2 (range); any other value MUST be rejected.                                                         |
| 1   | `direction`         | For `proof_type == 1`, MUST be 1 (GT) or 2 (LT). Ignored for range proofs.                                                    |
| 2   | `bound_lower`       | MUST NOT express a trivially-true or unsatisfiable claim (see below).                                                         |
| 3   | `bound_upper`       | For range proofs, MUST exceed `bound_lower` and MUST NOT exceed 10000.                                                        |
| 4   | `result`            | Positive result.                                                                                                              |
| 5   | `config_hash`       | MUST be registered and non-revoked in the config hash registry, and MUST NOT expand to any denied provider.                   |
| 6   | `provider_set_hash` | `--` Bound in-circuit to the committed providers and weights, but not cross-checked against any caller argument or registry.  |
| 7   | `submitter`         | Submitter binding.                                                                                                            |

Trivial-bound rejection: for `direction == GT`, `bound_lower == 0` or `bound_lower >= 10000`; for `direction == LT`, `bound_lower == 0` or `bound_lower > 10000`; for range proofs, the full domain `[0, 10000]`.

#### PATTERN (0x03)

| #   | Public input          | Oracle validation                                                                                                           |
| --- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| 0   | `analysis_type`       | MUST be 1 (anti-structuring), 2 (velocity), or 3 (round-amount). Consumers requiring a specific analysis check it themselves (see [Proof Types](#proof-types)). |
| 1   | `result`              | Positive result.                                                                                                            |
| 2   | `reporting_threshold` | MUST be registered in the reporting threshold registry.                                                                     |
| 3   | `time_window`         | MUST meet an implementation-defined minimum (reference: 3600 seconds).                                                      |
| 4   | `tx_set_hash`         | MUST be non-zero.                                                                                                           |
| 5   | `submitter`           | Submitter binding.                                                                                                          |
| 6   | `settlement_root`     | `--` Opaque; downstream binding is the consumer's job (see [Proof Types](#proof-types)).                                    |

#### ATTESTATION (0x04)

| #   | Public input        | Oracle validation                                                                                                             |
| --- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| 0   | `provider_id`       | MUST be non-zero and MUST equal the `providerId` recorded against the referenced credential root.                             |
| 1   | `credential_type`   | `--` Constrained in-circuit; the Oracle applies no additional check.                                                          |
| 2   | `is_valid`          | Positive result.                                                                                                              |
| 3   | `credential_root`   | MUST be registered in the credential root registry, MUST NOT be revoked, and MUST be within its TTL (reference: 48 hours).    |
| 4   | `current_timestamp` | Timestamp freshness; timestamp ratchet.                                                                                       |
| 5   | `submitter`         | Submitter binding.                                                                                                            |

#### MEMBERSHIP (0x05)

| #   | Public input  | Oracle validation                                                                                    |
| --- | ------------- | ------------------------------------------------------------------------------------------------------ |
| 0   | `merkle_root` | MUST be registered for MEMBERSHIP, and non-revoked, in the merkle root registry.                      |
| 1   | `set_id`      | `--` Bound in-circuit to the leaf hash; the Oracle applies no additional check.                       |
| 2   | `timestamp`   | Timestamp freshness; timestamp ratchet.                                                               |
| 3   | `is_member`   | Positive result.                                                                                      |
| 4   | `submitter`   | Submitter binding.                                                                                    |

#### NON_MEMBERSHIP (0x06)

| #   | Public input     | Oracle validation                                                                                 |
| --- | ---------------- | --------------------------------------------------------------------------------------------------- |
| 0   | `merkle_root`    | MUST be registered for NON_MEMBERSHIP, and non-revoked, in the merkle root registry.               |
| 1   | `set_id`         | `--` Bound in-circuit to the leaf hash; the Oracle applies no additional check.                    |
| 2   | `timestamp`      | Timestamp freshness; timestamp ratchet.                                                            |
| 3   | `is_non_member`  | Positive result.                                                                                   |
| 4   | `submitter`      | Submitter binding.                                                                                 |

#### COMPLIANCE_SIGNED (0x07)

| #   | Public input          | Oracle validation                                                                                            |
| --- | --------------------- | -------------------------------------------------------------------------------------------------------------- |
| 0   | `jurisdiction_id`     | MUST equal the `jurisdictionId` argument of `submitCompliance()`.                                             |
| 1   | `provider_set_hash`   | MUST equal the `providerSetHash` argument of `submitCompliance()`.                                            |
| 2   | `config_hash`         | MUST be registered and non-revoked in the config hash registry, and MUST NOT expand to any denied provider.   |
| 3   | `timestamp`           | Timestamp freshness; timestamp ratchet.                                                                       |
| 4   | `meets_threshold`     | Positive result.                                                                                              |
| 5   | `signer_pubkey_hash`  | MUST be registered in the signer pubkey hash registry, to a provider that is not denied.                      |
| 6   | `chain_id`            | MUST equal `block.chainid`.                                                                                   |
| 7   | `oracle_address`      | MUST equal `address(this)`.                                                                                   |
| 8   | `submitter`           | Submitter binding.                                                                                            |

#### RISK_SCORE_SIGNED (0x08)

| #   | Public input          | Oracle validation                                                                                            |
| --- | --------------------- | -------------------------------------------------------------------------------------------------------------- |
| 0   | `proof_type`          | As RISK_SCORE index 0.                                                                                        |
| 1   | `direction`           | As RISK_SCORE index 1.                                                                                        |
| 2   | `bound_lower`         | As RISK_SCORE index 2.                                                                                        |
| 3   | `bound_upper`         | As RISK_SCORE index 3.                                                                                        |
| 4   | `result`              | Positive result.                                                                                              |
| 5   | `config_hash`         | MUST be registered and non-revoked in the config hash registry, and MUST NOT expand to any denied provider.   |
| 6   | `provider_set_hash`   | `--` As RISK_SCORE index 6: bound in-circuit, not cross-checked at the submission boundary.                   |
| 7   | `timestamp`           | Timestamp freshness; timestamp ratchet.                                                                       |
| 8   | `signer_pubkey_hash`  | MUST be registered in the signer pubkey hash registry, to a provider that is not denied.                      |
| 9   | `chain_id`            | MUST equal `block.chainid`.                                                                                   |
| 10  | `oracle_address`      | MUST equal `address(this)`.                                                                                   |
| 11  | `submitter`           | Submitter binding.                                                                                            |

#### COMPLIANCE_MULTI_SIGNED (0x09)

| #   | Public input           | Oracle validation                                                                                                   |
| --- | ---------------------- | --------------------------------------------------------------------------------------------------------------------- |
| 0   | `jurisdiction_id`      | MUST equal the `jurisdictionId` argument of `submitCompliance()`.                                                    |
| 1   | `provider_set_hash`    | MUST equal the `providerSetHash` argument of `submitCompliance()`.                                                   |
| 2   | `config_hash`          | MUST be registered and non-revoked in the config hash registry, and MUST NOT expand to any denied provider.          |
| 3   | `timestamp`            | Timestamp freshness; timestamp ratchet.                                                                              |
| 4   | `meets_threshold`      | Positive result.                                                                                                     |
| 5   | `threshold_m`          | MUST be in `[1, MAX_PROVIDERS_MULTI]`, MUST NOT exceed the number of active signer slots, and MUST be at least the jurisdiction's `minMultiProviderThreshold`. |
| 6   | `signer_pubkey_hash_0` | If non-zero, MUST be registered in the signer pubkey hash registry, MUST be distinct from every other slot, and MUST belong to a different provider from every other active slot. |
| 7   | `signer_pubkey_hash_1` | As index 6.                                                                                                          |
| 8   | `signer_pubkey_hash_2` | As index 6.                                                                                                          |
| 9   | `signer_pubkey_hash_3` | As index 6.                                                                                                          |
| 10  | `signer_pubkey_hash_4` | As index 6.                                                                                                          |
| 11  | `chain_id`             | MUST equal `block.chainid`.                                                                                          |
| 12  | `oracle_address`       | MUST equal `address(this)`.                                                                                          |
| 13  | `submitter`            | Submitter binding.                                                                                                   |

### Proof Result Validation

Each proof type has a boolean result field (`meets_threshold`, `result`, `is_valid`, `is_member`, `is_non_member`). A valid proof with a false result proves the condition is NOT satisfied. Implementations MUST reject proofs where the result field is not `true` (encoded as `bytes32(uint256(1))`).

The `providerSetHash` parameter of `submitCompliance()` is meaningful only for COMPLIANCE, COMPLIANCE_SIGNED, and COMPLIANCE_MULTI_SIGNED. For those three, implementations MUST assert that it equals the proof's `provider_set_hash` public input, and MUST store it in the attestation. RISK_SCORE and RISK_SCORE_SIGNED bind `provider_set_hash` in-circuit only; other types have none. For every proof type outside the compliance family, implementations MUST ignore the caller-supplied `providerSetHash` and store `bytes32(0)` in the attestation.

### Validation Registries

Implementations MUST maintain on-chain registries for the values public inputs are validated against.

**Config hash registry.** Tracks valid provider weight configuration hashes, added when the administrator updates the configuration. Revocation rules are in [Provider Weight Publication](#provider-weight-publication).

**Merkle root registry.** Tracks valid merkle roots for MEMBERSHIP and NON_MEMBERSHIP proofs (e.g., sanctions lists, whitelists). Roots MUST be registered by the administrator before proofs referencing them can be accepted. Registration MUST be per proof type: a root registered for MEMBERSHIP MUST NOT satisfy NON_MEMBERSHIP, and the reverse. Both circuits share the `leaf_hash_subject` layout, so a shared registry would let a denylist root accept a MEMBERSHIP proof that the subject *is* on the denylist. 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. The publisher SHOULD publish new credential roots periodically, replacing prior ones. Roots SHOULD have a finite TTL window during which they are accepted, so users with paths against an outgoing root can still submit while a new root propagates. Implementations MUST verify the proof's `provider_id` matches the registered `providerId` for the referenced credential root.

**Reporting threshold registry.** Tracks valid reporting thresholds for PATTERN (anti-structuring) proofs; each jurisdiction defines its own (e.g., $10,000 under the United States Bank Secrecy Act (BSA)). Thresholds MUST be registered before proofs referencing them can be accepted.

**Signer pubkey hash registry.** Tracks the `H_signer_pubkey` commitments of providers authorized to sign screening payloads for the signed variants (0x07, 0x08, 0x09). Each registered commitment MUST be bound to the provider that operates the key; a provider MAY hold several keys. A `signer_pubkey_hash` public input that is not registered, or whose provider is denied, MUST be rejected. Compliance Multi-Signed (0x09) MUST count providers, not keys: two active slots whose keys belong to the same provider MUST be rejected, since one provider holding two keys (e.g., during rotation) would otherwise satisfy `threshold_m = 2` alone. The in-circuit ECDSA check proves only that *some* key signed; this registry makes it an authorized provider's key.

**Denied provider registry.** Tracks provider IDs that MUST NOT contribute to an accepted compliance proof, whether or not a configuration committing to them is still registered. Implementations MUST maintain this registry. They MUST record, for each registered config hash, the provider IDs that hash expands to, and MUST reject any proof whose `config_hash` (0x01, 0x02, 0x07, 0x08, 0x09) expands to a denied provider. Signed proofs are also rejected when their signer key belongs to a denied provider (see Signer pubkey hash registry). For the unsigned variants `provider_set_hash` is prover-chosen, so the denylist constrains only the registered configuration. The expansion MUST be recorded atomically with the configuration registration, so a partially-applied rotation cannot silently disable enforcement.

**Registry idempotency.** Across all registries, re-registering an already-registered value MUST revert, and revoking a value that is not registered MUST revert.

**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), and SHOULD support immediate credential root revocation (no delay) so a malicious root 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:

$$\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$, and $N \leq 8$ is the number of active providers. The result is in basis points ($0$-$10000$).

Circuits that accept `weight_sum` as a private input MUST constrain it to equal the actual sum of the `weights` array, so a prover cannot choose the denominator.

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)

Score integrity depends on provider independence. Implementations whose threat model includes collusion among a subset of providers SHOULD require attestations from multiple independent providers (e.g., COMPLIANCE_MULTI_SIGNED with `threshold_m >= 2`) and SHOULD weight providers by 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 an implementation-defined far-future bound MUST be rejected in-circuit; the reference rejects values of `2^40` (~year 36812) or more. When `timestamp` is a field element, the circuit MUST range-check the full value (e.g., to 64 bits) before narrowing it, since a truncating cast would accept `t + 2^64` for any in-range `t`. This applies to COMPLIANCE, COMPLIANCE_SIGNED, RISK_SCORE_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.

### 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 uses Pedersen from the Noir standard library.

Pedersen commitments are additively homomorphic over the underlying 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, which has stronger random-oracle properties, once its high-level APIs stabilize in the circuit language.

### Merkle Tree Domain Separation

Implementations MUST use distinct domain tags for leaf and internal-node hashes, so no leaf hash can collide with an internal node (second-preimage attack). The reference uses three tags: internal nodes, set-style leaves bound to `(element, set_id)`, and value-style leaves committing a single value (e.g., `credential_hash`).

Fixed-arity Pedersen does NOT domain-separate by arity (e.g., `H([a, b, 0]) == H([a, b])` without a length tag). Implementations MUST therefore include an explicit domain tag in the input array.

### Non-Membership Proof Security

The non-membership circuit proves the submitter is NOT in a sorted Merkle tree by adjacency: two consecutive leaves $l$ and $h$ satisfy $l < \text{submitter} < h$ AND $\text{high\_index} = \text{low\_index} + 1$.

Without adjacency, two non-adjacent entries could bracket the submitter and hide an intermediate entry equal to the submitter. 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 neighbors.

Comparison MUST be performed over the full Field range using bit-decomposition (e.g., Noir's `Field::lt`; see [Reference Implementation](#reference-implementation)), so arbitrary-width identifiers (addresses, hashes) compare without truncation.

### Submitter Binding

Implementations MUST bind every proof to its submitter via the `submitter` public input. The comparison MUST cover the full 32-byte public-input word: `submitter` MUST equal `uint256(uint160(msg.sender))` exactly, and a value that matches `msg.sender` only in its low 160 bits MUST be rejected, because the circuit proves its claim about the full field value. For proofs about a specific party (membership, non-membership, attestation), the leaf format MUST also bind to `submitter` in-circuit:

- 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)`.

### 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

Counterparties to retroactively flagged addresses can present the original attestation (via `getHistoricalProof()`) as proof-of-innocence that the address was clean at transaction time.

### Pause Mechanism

Implementations SHOULD support pausing proof submission, to contain a circuit or verifier vulnerability without redeploying. Pause MUST NOT block read access to existing attestations (`checkCompliance`, `checkComplianceByType`, `getHistoricalProof`, `getAttestationHistory`), since proof-of-innocence depends on them. Implementations SHOULD support per-proof-type pause in addition to global pause.

### 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. Implementations SHOULD timelock critical operations (verifier replacement, TTL changes, weight updates) in production. Implementations SHOULD split administrative authority into role classes (e.g., 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 uses three roles (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, so integrators and auditors can compare the published policy against 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. With client-side proving the raw data never leaves the user's device; the verifier learns only the boolean result.

**Why published weights?** Opaque compliance algorithms invite regulatory skepticism and legal challenge. Published weights and thresholds make the system auditable without exposing individuals, and weight adjustments are visible.

**Why on-chain attestations?** Off-chain attestations can be forged, lost, or denied. Proof-of-innocence needs a record retrievable years after the transaction.

**Why not Privacy Pools inclusion/exclusion proofs?** Privacy Pools prove set membership ("I'm not in the OFAC set"). This ERC proves rule compliance ("my risk score under jurisdiction X is below threshold Y using providers A, B, C"). Set membership is a subset of regulatory compliance.

**Why attestation TTL?** Compliance status changes as providers update their data. The TTL forces periodic re-attestation, with the window configurable per deployment.

**Why nine proof types?** Each type is a separate circuit with distinct constraint logic. The signed variants 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[]` witness, while a signed proof attests them. Separation keeps circuits small and auditable and lets unsigned-tolerant jurisdictions avoid the signature-verification cost.

### What this standard does NOT prove

The cryptographic guarantees cover _correct computation_, not _honest inputs_. Three trust tiers exist; integrators pick the tier matching 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 suits jurisdictions that permit user-asserted compliance (some EU and UK contexts), flows where a downstream system checks signal honesty, and composed proofs. A self-attested COMPLIANCE submitter can 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 a deliberate tradeoff.

Normative counterparts: [Public Input Validation](#public-input-validation) and [Trust Tier Disclosure](#trust-tier-disclosure).

### Related Work

**[ERC-3643](./eip-3643.md) (T-REX).** The compliance token standard for regulated securities. It requires identity revelation via on-chain identity claims verified by trusted issuers; this ERC proves compliance without revealing identity, signals, or amounts. This ERC could serve as a ZK identity provider within an ERC-3643 deployment.

**Privacy pools.** Users prove a withdrawal originates from a "clean" deposit set, with association set providers maintaining approved lists. This ERC extends the model beyond set membership to risk scoring, anti-structuring, credential verification, and membership/non-membership proofs.

**Proof-gated token transfers.** An oracle-permissioned [ERC-20](./eip-20.md) can validate each transfer with a zkVM proof against off-chain ISO 20022 payment instructions, gating one token through one oracle with one proof type. This ERC provides standalone attestations with nine proof types, usable by any contract.

**[ERC-7812](./eip-7812.md).** A ZK identity registry: a singleton Sparse Merkle Tree (80-level, Poseidon on BN128) with custom registrars, deployed on mainnet. This ERC could operate as a compliance registrar within it.

**Smart-account ZK verifier interfaces.** A proof-system-agnostic verifier interface for smart accounts (e.g., `verifyProof(bytes,bytes) returns (bytes4)`) can use a non-reverting return pattern following [ERC-1271](./eip-1271.md). This ERC's routing adds domain-specific semantics (proof type routing, batch verification, version history); each UltraHonk verifier could be wrapped behind such an adapter.

**[EIP-7702](./eip-7702.md).** When a 7702-delegated EOA calls `submitCompliance`, `msg.sender` is the EOA, so the `submitter` public input must equal the EOA. A 7702 batcher can call `submitComplianceBatch` provided every entry's `submitter` equals the delegating EOA. Account-abstraction wallets should show the bound `submitter` to the user before submission, since a malicious dApp could otherwise solicit proofs bound to the wrong address. The same applies to [ERC-4337](./eip-4337.md) paymasters and ERC-1271 contract signers used as compliance subjects.

**ZK credential anchors.** Non-transferable credential anchors with ZK presentation support predicate proofs ("score >= threshold") that parallel RISK_SCORE. Such systems target credential issuance and presentation; this ERC targets compliance attestation and retroactive verification.

**Generic zk-SNARK verifier interface (2019, stagnant).** Defines a generic on-chain verification interface with dynamic arrays. This ERC instead uses per-proof-type routing, UltraHonk support, and domain-specific input validation.

## Backwards Compatibility

This ERC introduces new interfaces and modifies no existing standard. It complements, but does not depend on, [ERC-5564](./eip-5564.md) (stealth addresses) and [ERC-6538](./eip-6538.md) (stealth meta-address registry).

## Test Cases

Binary fixtures for all nine proof types are in `test/fixtures/<circuit>/` of the [reference implementation](../assets/eip-8262/README.md). The three signed fixtures (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED, COMPLIANCE_MULTI_SIGNED) are signed by public test keys and bind chain ID 31337 and the reference integration test's Oracle address, so only that deployment accepts them. Every fixture uses `timestamp = 1700000000`, so the freshness rule accepts timestamped fixtures only while `block.timestamp` is in `[1700000000, 1700003600]`. Each fixture contains:

- `proof`: the raw UltraHonk proof bytes (8640 bytes for the unsigned types; 9024 bytes for COMPLIANCE_SIGNED and RISK_SCORE_SIGNED; 9792 bytes for COMPLIANCE_MULTI_SIGNED)
- `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                                                                                                    |
| COMPLIANCE_SIGNED       | 288 bytes (9 inputs)  | jurisdiction_id, provider_set_hash, config_hash, timestamp, meets_threshold, signer_pubkey_hash, chain_id, oracle_address, submitter                        |
| RISK_SCORE_SIGNED       | 384 bytes (12 inputs) | proof_type, direction, bound_lower, bound_upper, result, config_hash, provider_set_hash, timestamp, signer_pubkey_hash, chain_id, oracle_address, submitter |
| COMPLIANCE_MULTI_SIGNED | 448 bytes (14 inputs) | jurisdiction_id, provider_set_hash, config_hash, timestamp, meets_threshold, threshold_m, signer_pubkey_hash_0..4, chain_id, oracle_address, submitter      |

Fixtures use Pedersen hash (Noir stdlib) for commitments and Merkle trees, and are regenerated via [`scripts/generate-fixtures.sh`](../assets/eip-8262/scripts/generate-fixtures.sh).

### Witness Annex

The Prover.toml inputs behind the binary fixtures, for cross-validation. All `submitter` values are `0xdead` and all `timestamp` values are `1700000000` (2023-11-14). Addresses are packed as field elements; Pedersen hashes are over BN254.

```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.
```

```toml
# circuits/risk_score_signed/Prover.toml (provider-signed "risk < 50%")
signals            = [25, 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
signature          = [182, 115, 199, 138, 49, 136, 65, 189, 35, 71, 72, 208, 193, 89, 65, 185,
                      16, 157, 119, 27, 54, 175, 210, 53, 18, 123, 253, 65, 234, 199, 103, 2,
                      112, 233, 231, 138, 229, 193, 168, 120, 241, 198, 7, 173, 114, 19, 157, 40,
                      160, 61, 75, 20, 243, 126, 101, 201, 54, 138, 59, 255, 192, 4, 184, 81]
pubkey_x           = [32, 200, 84, 126, 134, 190, 235, 89, 194, 183, 197, 14, 72, 83, 155, 145,
                      70, 127, 150, 181, 178, 60, 72, 184, 9, 78, 30, 217, 164, 212, 226, 66]
pubkey_y           = [45, 74, 60, 178, 139, 138, 55, 189, 66, 187, 156, 255, 146, 98, 182, 228,
                      212, 219, 184, 140, 139, 128, 126, 35, 147, 85, 247, 92, 95, 166, 66, 144]
proof_type         = 1     # threshold
direction          = 2     # LT
bound_lower        = 5000  # asserts score < 5000 bps
bound_upper        = 0
result             = true
config_hash        = "0x18574f427f33c6c77af53be06544bd749c9a1db855599d950af61ea613df8405"
provider_set_hash  = "0x14b6becf762f80a24078e62fc9a7eca246b8e406d19962dda817b173f30a94b2"
timestamp          = "1700000000"
signer_pubkey_hash = "0x294fc7a4edbc711b1c3ceceb89474957aea2974dbf3d27efe759a1ecef35d6dc"
chain_id           = "31337"
oracle_address     = "0x2e234dae75c793f67a35089c9d99245e1c58470b"
submitter          = "0xdead"
# Signer: secp256k1 key keccak256("erc8262-integration-fixture-provider-signer"), a public
# test key. signature = low-S ECDSA over
# H_signed(DOMAIN_RISK_SIGNED_SIGNALS, 31337, oracle_address, provider_set_hash, signals,
#          weights, 1700000000, 0xdead)
#       = 0x0f0806548e18a3616c34e8852e1919d908f9cf1ccd5cf814c83b534d10dd9fe1.
```

```toml
# circuits/compliance_signed/Prover.toml (provider-signed, US)
signals            = [25, 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
signature          = [138, 191, 70, 221, 230, 120, 2, 250, 16, 119, 25, 190, 110, 156, 167, 62,
                      113, 176, 118, 43, 122, 51, 197, 253, 239, 146, 7, 22, 71, 34, 104, 83,
                      3, 205, 251, 124, 84, 47, 205, 235, 247, 215, 233, 248, 116, 233, 138, 165,
                      178, 75, 162, 192, 132, 92, 141, 148, 127, 185, 82, 141, 99, 202, 106, 170]
pubkey_x           = [32, 200, 84, 126, 134, 190, 235, 89, 194, 183, 197, 14, 72, 83, 155, 145,
                      70, 127, 150, 181, 178, 60, 72, 184, 9, 78, 30, 217, 164, 212, 226, 66]
pubkey_y           = [45, 74, 60, 178, 139, 138, 55, 189, 66, 187, 156, 255, 146, 98, 182, 228,
                      212, 219, 184, 140, 139, 128, 126, 35, 147, 85, 247, 92, 95, 166, 66, 144]
jurisdiction_id    = 1     # US
provider_set_hash  = "0x14b6becf762f80a24078e62fc9a7eca246b8e406d19962dda817b173f30a94b2"
config_hash        = "0x18574f427f33c6c77af53be06544bd749c9a1db855599d950af61ea613df8405"
timestamp          = "1700000000"
meets_threshold    = true
signer_pubkey_hash = "0x294fc7a4edbc711b1c3ceceb89474957aea2974dbf3d27efe759a1ecef35d6dc"
chain_id           = "31337"
oracle_address     = "0x2e234dae75c793f67a35089c9d99245e1c58470b"
submitter          = "0xdead"
# Signer: the RISK_SCORE_SIGNED test key. signature = low-S ECDSA over
# H_signed(DOMAIN_SIGNED_SIGNALS, 31337, oracle_address, provider_set_hash, signals,
#          weights, 1700000000, 0xdead).
```

```toml
# circuits/compliance_multi_signed/Prover.toml (US 2-of-5, slots 0 and 2 active)
signals              = [[25, 0, 0, 0, 0, 0, 0, 0],
                        [0, 0, 0, 0, 0, 0, 0, 0],
                        [25, 0, 0, 0, 0, 0, 0, 0],
                        [0, 0, 0, 0, 0, 0, 0, 0],
                        [0, 0, 0, 0, 0, 0, 0, 0]]
weights              = [[100, 0, 0, 0, 0, 0, 0, 0],
                        [1, 0, 0, 0, 0, 0, 0, 0],
                        [100, 0, 0, 0, 0, 0, 0, 0],
                        [1, 0, 0, 0, 0, 0, 0, 0],
                        [1, 0, 0, 0, 0, 0, 0, 0]]
weight_sums          = [100, 1, 100, 1, 1]
pubkey_xs            = [[32, 200, 84, 126, 134, 190, 235, 89, 194, 183, 197, 14, 72, 83, 155, 145,
                         70, 127, 150, 181, 178, 60, 72, 184, 9, 78, 30, 217, 164, 212, 226, 66],
                        [198, 4, 127, 148, 65, 237, 125, 109, 48, 69, 64, 110, 149, 192, 124, 216,
                         92, 119, 142, 75, 140, 239, 60, 167, 171, 172, 9, 185, 92, 112, 158, 229],
                        [253, 204, 83, 103, 21, 78, 54, 83, 31, 229, 89, 136, 198, 83, 192, 36,
                         97, 182, 29, 228, 19, 233, 173, 0, 15, 134, 197, 86, 168, 195, 17, 190],
                        [198, 4, 127, 148, 65, 237, 125, 109, 48, 69, 64, 110, 149, 192, 124, 216,
                         92, 119, 142, 75, 140, 239, 60, 167, 171, 172, 9, 185, 92, 112, 158, 229],
                        [198, 4, 127, 148, 65, 237, 125, 109, 48, 69, 64, 110, 149, 192, 124, 216,
                         92, 119, 142, 75, 140, 239, 60, 167, 171, 172, 9, 185, 92, 112, 158, 229]]
pubkey_ys            = [[45, 74, 60, 178, 139, 138, 55, 189, 66, 187, 156, 255, 146, 98, 182, 228,
                         212, 219, 184, 140, 139, 128, 126, 35, 147, 85, 247, 92, 95, 166, 66, 144],
                        [26, 225, 104, 254, 166, 61, 195, 57, 163, 197, 132, 25, 70, 108, 234, 238,
                         247, 246, 50, 101, 50, 102, 208, 225, 35, 100, 49, 169, 80, 207, 229, 42],
                        [15, 246, 126, 99, 123, 111, 128, 191, 152, 137, 238, 22, 206, 92, 15, 114,
                         232, 218, 126, 206, 193, 36, 184, 169, 129, 187, 231, 144, 86, 75, 30, 6],
                        [26, 225, 104, 254, 166, 61, 195, 57, 163, 197, 132, 25, 70, 108, 234, 238,
                         247, 246, 50, 101, 50, 102, 208, 225, 35, 100, 49, 169, 80, 207, 229, 42],
                        [26, 225, 104, 254, 166, 61, 195, 57, 163, 197, 132, 25, 70, 108, 234, 238,
                         247, 246, 50, 101, 50, 102, 208, 225, 35, 100, 49, 169, 80, 207, 229, 42]]
signatures           = [[204, 73, 40, 53, 125, 29, 157, 48, 165, 157, 193, 191, 52, 146, 252, 105,
                         82, 168, 151, 186, 220, 169, 12, 75, 41, 199, 95, 153, 214, 33, 75, 64,
                         123, 156, 210, 17, 96, 88, 221, 84, 247, 242, 142, 57, 218, 156, 15, 172,
                         96, 242, 138, 212, 197, 181, 83, 199, 103, 184, 237, 244, 242, 164, 224, 53],
                        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                        [88, 175, 244, 218, 171, 130, 90, 176, 10, 110, 162, 173, 198, 210, 91, 167,
                         35, 0, 184, 82, 108, 214, 1, 96, 247, 50, 122, 115, 33, 187, 127, 37,
                         127, 160, 104, 4, 235, 108, 16, 182, 1, 18, 206, 203, 160, 162, 96, 114,
                         254, 136, 27, 176, 160, 192, 26, 121, 170, 113, 246, 44, 189, 55, 71, 68],
                        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]]
jurisdiction_id      = 1     # US
provider_set_hash    = "0x14b6becf762f80a24078e62fc9a7eca246b8e406d19962dda817b173f30a94b2"
config_hash          = "0x18574f427f33c6c77af53be06544bd749c9a1db855599d950af61ea613df8405"
timestamp            = "1700000000"
meets_threshold      = true
threshold_m          = 2     # US floor
signer_pubkey_hash_0 = "0x294fc7a4edbc711b1c3ceceb89474957aea2974dbf3d27efe759a1ecef35d6dc"
signer_pubkey_hash_1 = "0"
signer_pubkey_hash_2 = "0x2eb905640e16a7a7cb2fc08f412e4db744621a9b8ea6c760cedeefc5004e1b20"
signer_pubkey_hash_3 = "0"
signer_pubkey_hash_4 = "0"
chain_id             = "31337"
oracle_address       = "0x2e234dae75c793f67a35089c9d99245e1c58470b"
submitter            = "0xdead"
# Slot 0 signer: the RISK_SCORE_SIGNED test key. Slot 2 signer: secp256k1 key
# keccak256("erc8262-integration-fixture-provider-signer-2"), a public test key.
# Each active slot signs H_multi_slot(slot_index, 31337, oracle_address, 1,
#   provider_set_hash, config_hash, signals[i], weights[i], 1700000000, 0xdead).
# Inactive slots (1, 3, 4): signer_pubkey_hash 0, signals 0, weights [1, 0, ...],
# weight_sum 1, pubkey 2G, signature r = s = 1.
```

Signed-variant witnesses (COMPLIANCE_SIGNED, RISK_SCORE_SIGNED) match their unsigned siblings in the screening payload, plus `pubkey_x`, `pubkey_y`, `signature`, `signer_pubkey_hash`, `chain_id`, and `oracle_address` (RISK_SCORE_SIGNED also adds the public `timestamp`). The provider signs `H_signed(domain, chain_id, oracle_address, provider_set_hash, signals, weights, timestamp, submitter)` off-chain with the proof type's domain tag. In COMPLIANCE_MULTI_SIGNED (0x09) each active slot supplies its own `(signals, weights, weight_sum, pubkey_x, pubkey_y, signature)` and non-zero `signer_pubkey_hash`, signing its slot-specific `H_multi_slot` digest. The signed fixtures carry precomputed signatures from public test keys; implementations producing new signed fixtures must sample fresh ECDSA nonces.

## Reference Implementation

A reference implementation is in the [assets directory](../assets/eip-8262/README.md). It covers the oracle, the verifier router, the interfaces, the COMPLIANCE (0x01) circuit, and the fixtures for all nine proof types; the other eight circuits follow [Per-Type Circuit Specifications](#per-type-circuit-specifications).

- Oracle: [`ERC8262Oracle.sol`](../assets/eip-8262/contracts/ERC8262Oracle.sol). Proof submission, per-proof-type semantic validation, attestation storage and lifecycle, and the validation registries. Each proof type's `_validate*Inputs` function lists its public inputs at their byte offsets and is the normative counterpart to [Public Input Validation](#public-input-validation).
- Router: [`ERC8262Verifier.sol`](../assets/eip-8262/contracts/ERC8262Verifier.sol). Per-proof-type verifier registry, append-only version history, timelocked upgrades, and batch verification (Foundry, Solidity 0.8.28, Cancun EVM). The router validates only public-input shape (alignment and count, via `ProofTypes.validatePublicInputs`); the Oracle does all semantic validation before calling it.
- Interfaces and libraries: the [verifier interface](../assets/eip-8262/contracts/interfaces/IERC8262Verifier.sol), [oracle interface](../assets/eip-8262/contracts/interfaces/IERC8262Oracle.sol), [proof-type IDs and public-input shape validation](../assets/eip-8262/contracts/libraries/ProofTypes.sol), [per-jurisdiction policy](../assets/eip-8262/contracts/libraries/JurisdictionConfig.sol), and [access-control roles](../assets/eip-8262/contracts/libraries/AccessControl.sol).
- Circuit: the COMPLIANCE (0x01) [main.nr](../assets/eip-8262/circuits/compliance/src/main.nr), in Noir, compiled to an UltraHonk verifier by Barretenberg. Pinned tool versions and licenses are in the [reference implementation README](../assets/eip-8262/README.md).
- Fixtures: proofs and public inputs for all nine proof types under `test/fixtures/`, regenerated from each circuit's `Prover.toml` by [`generate-fixtures.sh`](../assets/eip-8262/scripts/generate-fixtures.sh) (see [Test Cases](#test-cases)).

Per-circuit UltraHonk verifiers (~100 KB of Solidity each) are build artifacts reproducible from the circuits; they are omitted here and registered into the router by address at deploy time.

Some raw `bb`-generated verifiers exceed the [EIP-170](./eip-170.md) 24,576-byte runtime limit. Rewriting the `pairing()` free function in inline Yul saves ~186 bytes per verifier (and ~800 gas per `verifyProof` call) with semantics identical to `bb` output on the pairing precompile (`address(0x08)`) input layout.

## Security Considerations

**Proof soundness.** Security reduces to the soundness of the ZK proof system; a weak system lets a prover forge a passing attestation without satisfying the circuit. [Proof System Requirements](#proof-system-requirements) sets the 128-bit floor.

**Provider collusion.** Colluding providers holding a weighted majority (or a sole provider under unsigned COMPLIANCE) can issue false clean signals undetectably. COMPLIANCE_MULTI_SIGNED (0x09) with `threshold_m >= 2` is the strongest in-protocol defense, with `minMultiProviderThreshold` as a per-jurisdiction floor ([Risk Score Computation](#risk-score-computation)).

**Timestamp manipulation.** Block proposers control `block.timestamp` within the parent-block constraint: acceptable for day-scale compliance windows, not for fine-grained ordering or liveness. Integrators that need finer-grained ordering must use an explicit nonce or sequence number. In-circuit bounds are in [Circuit Constraints](#circuit-constraints).

**Regulatory acceptance.** Whether a jurisdiction accepts ZK proofs as compliance evidence is a legal question. The VARA (Dubai) definition of "anonymity-enhanced crypto" excludes assets with "mitigating technologies" for traceability, which this standard provides.

**Front-running the oracle.** A proof submission in the mempool signals an imminent trade, though its details stay hidden. Integrators can batch submissions or submit within the settlement transaction; this is deployment policy, not a requirement.

**Administrative operations.** A single compromised admin key could pause the Oracle and rewrite its registries in one transaction; see [Administrative Operations](#administrative-operations). The reference role split is in [`AccessControl.sol`](../assets/eip-8262/contracts/libraries/AccessControl.sol), and its verifier upgrade and revocation delays (24 h and 6 h) are in [`ERC8262Verifier.sol`](../assets/eip-8262/contracts/ERC8262Verifier.sol).

**Public input validation.** Unvalidated inputs let a prover replay a proof from a lenient context (e.g., a permissive reporting threshold) into a stricter one ([Public Input Validation](#public-input-validation)). A proof with a false result field is a valid proof of non-compliance; accepting it would record a compliant attestation for a non-compliant subject ([Proof Result Validation](#proof-result-validation)).

**Proof replay prevention.** [Proof Hash Computation](#proof-hash-computation) scopes attestations to one `(chain, Oracle, proofType)`, and the `_usedProofs` guard blocks resubmission of the same proof within one Oracle. In-circuit chain/Oracle binding exists only for the signed variants ([Public Input Validation](#public-input-validation)).

**Signed-payload type binding and expiry.** `_usedProofs` rejects repeated proof bytes, not repeated signed payloads: one provider `signature` can back any number of distinct proofs. Two properties bound it. [Commitment Layouts](#commitment-layouts) gives each signed type its own `H_signed` domain tag, so a Compliance Signed (0x07) payload cannot satisfy Risk Score Signed (0x08) or the reverse. Both variants expose the signed `timestamp`, which [Public Input Validation](#public-input-validation) holds to the maximum proof age and uses as the ratchet value, so a `signature` stops minting attestations once that window passes. Without both, any 0x08 `signature` a provider ever issued, or any 0x07 bundle, could mint fresh 0x08 attestations indefinitely.

**Config and root revocation.** Without revocation, a flawed configuration or compromised merkle tree stays accepted forever. Rules are in [Provider Weight Publication](#provider-weight-publication) and [Validation Registries](#validation-registries).

**Verifier TOCTOU.** A verifier upgrade landing between resolution and verification could record a `verifierUsed` other than the verifier that validated the proof; [Verifier Versioning](#verifier-versioning) requires single resolution.

**Batch verification limits.** Batch cost grows linearly (~2.4 M gas per UltraHonk proof); [Batch Verification Limits](#batch-verification-limits) requires a cap. Reference figures, measured on Cancun EVM with the [Test Cases](#test-cases) fixtures; other proof systems and circuits 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 cost is dominated by in-circuit ECDSA-secp256k1, which roughly doubles off-chain proving time; on-chain `verifyProof` stays the same order of magnitude as the unsigned variants. Chains with larger block budgets can raise the batch cap; chains with smaller 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; `submitComplianceBatch` amortizes the per-entry fixed cost.

**Registry idempotency.** A silent no-op on duplicate registration or missing revocation could leave operators believing a state change landed ([Validation Registries](#validation-registries)).

**Emergency circuit break.** [Pause Mechanism](#pause-mechanism) contains a circuit or verifier vulnerability without redeploying, keeping proof-of-innocence reads live; per-proof-type pause limits the blast radius.

**Trust model and signal honesty.** The unsigned variants (COMPLIANCE, RISK_SCORE) are self-attested: the circuit verifies the score formula, not the signals ([What this standard does NOT prove](#what-this-standard-does-not-prove)). The per-jurisdiction policy ([Public Input Validation](#public-input-validation)) rejects them in strict-mode jurisdictions (US BSA, Singapore, UAE VARA in the reference). Integrators in permissive jurisdictions needing signal honesty should require the signed variants, optionally composed with ATTESTATION against an independently published credential tree ([Trust Tier Disclosure](#trust-tier-disclosure)).

**ATTESTATION authority root.** The credential leaf `H(DOMAIN_CREDENTIAL, provider_id, submitter, type, attribute, expiry)` binds the credential to the submitter; forging a leaf requires breaking Pedersen preimage resistance. No provider signature over the leaf or root is verified in-circuit: authority resolves to the registered publisher EOA ([Validation Registries](#validation-registries)). A compromised publisher EOA can publish arbitrary `(submitter, attribute)` pairs until the owner rotates the publisher or revokes the root. Deployments with that threat can layer an in-circuit signature over the root or leaf; this specification does not require it.

**Cross-chain replay.** The unsigned proof types (COMPLIANCE, RISK_SCORE, PATTERN, ATTESTATION, MEMBERSHIP, NON_MEMBERSHIP) have no chain identifier public input, so the same proof bytes can be replayed on another chain or against another Oracle deployment, producing independent attestations. The proof-hash guard ([Proof Hash Computation](#proof-hash-computation)) blocks replay only within one (chain, Oracle) pair. The signed variants' digest commits to `(chain_id, oracle_address, ...)` and [Public Input Validation](#public-input-validation) checks both, so replaying them requires forging an ECDSA signature. Deployments that need this for unsigned proofs can fork the unsigned circuits to add `chain_id` and `oracle_address` public inputs, at the cost of a new verifier per chain.

**Verifier-layer reentrancy.** A non-`view` verifier could reenter the Oracle and mutate attestation state; [Verifier Interface](#verifier-interface) requires `view`, so the call uses `STATICCALL`.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
