Proof of Innocence
Proof of Innocence is an off-chain compliance feature that acts as an AML policy. It allows users to prove their received funds did not originate from sanctioned addresses (e.g., OFAC list) without revealing transaction details.
Multi-layered compliance: zERC20 enforces sanctions compliance at two layers. The on-chain Blocklist contract prevents sanctioned addresses from participating in any token operation (transfer, mint, burn, teleport). Proof of Innocence adds a second layer by enabling recipients to cryptographically prove that none of their received funds originated from sanctioned sources, even when the sender's identity is hidden by the privacy mechanism.
Overview
Purpose
Prove sender addresses are not on a sanctions list
Proof System
Nova IVC proof
Verification
Off-chain CLI verifier
Data Structure
Sparse Merkle exclusion tree for sanctions list
How It Works
┌─────────────────────────────────────────────────────────────────┐
│ Proof of Innocence Flow │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Prove that `totalTeleported` per recipient (hash of │
│ `GeneralRecipient`) is not originated from OFAC-sanctioned │
│ sources. │
│ ↓ │
│ 2. Commit the OFAC list using a commitment scheme that │
│ supports non-membership proofs (e.g., Exclusion Tree). │
│ ↓ │
│ 3. For each `from_address`, generate non-membership proof. │
│ ↓ │
│ 4. For each teleport, prove that `transfer_leaf.from` is not │
│ in the OFAC list, then aggregate the steps with Nova. │
│ ↓ │
│ 5. The CLI verifier checks the Nova proof and confirms public │
│ state: `recipient`, `totalTeleported`, `ofacRoot`, and │
│ `transferRoot`. │
│ │
└─────────────────────────────────────────────────────────────────┘Cryptographic Design
Exclusion Tree
A “OFAC non-membership tree” is a Merkle tree of sorted disjoint (start, end) pairs representing the gaps between the elements of the OFAC sanctions list. The gap boundaries are exclusive: an address is proven innocent only when start < from_address < end. Sanctioned addresses sit on gap boundaries and therefore do not satisfy any gap. The OFAC sanctions list is committed using an OFAC non-membership tree. The leaf of the tree is computed as:
If the sanctions list contains k addresses, the exclusion tree has k + 1 gaps (one before the first address, one between each consecutive pair, and one after the last). zERC20 fixes the OFAC exclusion tree height at 32 for the current proof_of_innocence circuit artifacts. The sparse tree does not allocate all 2^32 leaves; proof and circuit cost grow roughly with the Merkle path length.
Non-Membership Proof (Nova Step)
Each transfer to a recipient has a transfer_leaf = (from_address, burn_address, value) (where burn_address is the to field of the generic transfer leaf). To prove that totalTeleported per recipient is not originated from sanctioned sources, for each transfer we prove the sender (transfer_leaf.from) is not in the OFAC list: Public Inputs (constant across all steps):
ofac_root- root of the OFAC non-membership treerecipient- hash of GeneralRecipienttransfer_root- root of the transfer tree containing the proved transfer leaves Accumulator State:totalTeleported- running sum of transfer values Per-Step Private Inputs:from_address- sender of this transfer (transfer_leaf.from)value- transfer amountsecret- burn-address secret binding the transfer torecipientleaf_index,transfer_siblings- transfer-tree Merkle witnessstart,end- gap boundaries containingfrom_addressgap_index,siblings- Merkle proof for the OFAC exclusion gap Per-Step Constraints:Recipient binding:
secretderives the transfer burn address forrecipientTransfer inclusion: the transfer leaf is included in
transfer_rootSender not sanctioned:
start < from_address < endverified againstofac_rootAnti-replay ordering:
leaf_indexincreases strictly across stepsAccumulate value:
totalTeleported_new = totalTeleported_old + value
CLI Usage
Note: This is an MVP interface. Users must manually assemble witness files.
Generate Proof of Innocence
Generate a Nova proof that all transfers to a recipient originated from non-sanctioned addresses.
--nova-artifacts-dir
Directory containing proof_of_innocence_nova_pp.bin and proof_of_innocence_nova_vp.bin; can also be set with NOVA_ARTIFACTS_DIR
--recipient
Hash of GeneralRecipient (from withdrawal flow)
--ofac-root
Trusted OFAC exclusion tree root
--transfer-root
Transfer tree root for all transfer witnesses
--transfers-file
JSON file with complete transfer witnesses
--exclusion-proofs-file
JSON file with exclusion proofs from OFAC tree service
--output
Path to write canonical uncompressed Nova IVC proof bytes
Verify Proof of Innocence
--nova-artifacts-dir
Directory containing proof_of_innocence_nova_pp.bin and proof_of_innocence_nova_vp.bin; can also be set with NOVA_ARTIFACTS_DIR
--proof
Path to the proof file
--recipient
Expected recipient hash
--total-teleported
Expected total value (wei)
--ofac-root
Trusted OFAC exclusion tree root
--transfer-root
Expected transfer tree root
Example
Input File Formats
The two JSON arrays must have the same length. Entries are paired by array position, then the CLI sorts the paired witnesses by leaf_index before proving. Each paired entry must use the same from_address.
transfers.json - List of transfers to prove:
exclusion_proofs.json - Exclusion proofs from OFAC tree service:
transfer_siblings must contain 40 field elements. siblings must contain 32 field elements for the current OFAC circuit.
Last updated
Was this helpful?