Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
LABS
Guides

Setting Up a Zero-Knowledge Proof System for AML Screening

A technical tutorial for developers to implement a privacy-preserving AML screening system using zero-knowledge proofs. Covers circuit logic, list verification, and on-chain verification.
Chainscore © 2026
introduction
INTRODUCTION

Setting Up a Zero-Knowledge Proof System for AML Screening

Learn how to build a privacy-preserving Anti-Money Laundering (AML) screening system using zero-knowledge proofs (ZKPs) to verify compliance without exposing sensitive transaction data.

Traditional Anti-Money Laundering (AML) compliance requires financial institutions to share sensitive customer transaction data with regulators and third-party screeners, creating significant privacy risks and data silos. Zero-knowledge proofs (ZKPs) offer a cryptographic solution, enabling an entity to prove a statement is true—such as "this transaction is not linked to a sanctioned address"—without revealing the underlying data. This guide explains how to implement a ZKP-based AML screening system using tools like Circom for circuit design and SnarkJS for proof generation and verification.

The core of a ZKP AML system is the circuit, a program that defines the computational statement to be proven. For screening, this circuit would encode the logic of checking a transaction against a sanctions list. For example, using the Circom language, you could create a circuit that takes a private input (the transaction recipient's address) and a public input (a Merkle root of the sanctions list) and outputs a proof that the address is not a member of that list. The critical property is that the prover never reveals the actual address to the verifier.

A practical implementation involves several steps. First, a trusted authority, like a regulator, maintains and periodically publishes a Merkle tree root of the current sanctions list. Financial institutions then use this public root within their ZKP circuit. When a transaction occurs, the institution generates a proof that the recipient's address hashes to a leaf that is not included in the tree with that root. This proof, along with the public root, is submitted for verification. Libraries such as zk-SNARKs via SnarkJS or zk-STARKs via StarkWare's Cairo are commonly used for this proof layer.

Integrating this system requires careful architecture. The backend must handle circuit compilation, trusted setup (if using SNARKs), witness calculation (generating the private inputs for the proof), and proof generation. The verification component, which is lightweight, can be deployed as a smart contract on a blockchain like Ethereum, allowing for decentralized and tamper-proof compliance checks. This creates an audit trail where only the proof's validity is recorded on-chain, not the sensitive data.

This approach addresses key regulatory challenges. It enables selective disclosure for audits, where a financial institution can reveal the specific transaction data to a regulator under specific conditions while keeping it hidden from all other parties. Furthermore, it facilitates secure inter-institutional checks; banks can prove a customer is not sanctioned when onboarding them from another bank without directly sharing the customer's full history. This balances the needs for financial privacy and regulatory compliance.

To begin development, start with the Circom documentation and tutorial to design a simple non-membership circuit. Use test frameworks like Hardhat or Foundry to simulate the verification process on a local Ethereum network. The final system must be rigorously audited, as bugs in ZKP circuits can lead to false proofs of compliance. This technical foundation enables a new paradigm for privacy-preserving finance.

prerequisites
ZK-AML TUTORIAL

Prerequisites and Setup

This guide outlines the technical prerequisites for building a zero-knowledge proof system for Anti-Money Laundering (AML) screening, focusing on the tools and foundational knowledge required.

Before implementing a ZK-AML system, you need a solid understanding of core cryptographic concepts. You should be familiar with zero-knowledge proofs (ZKPs), particularly zk-SNARKs and zk-STARKs, which allow you to prove a transaction's compliance without revealing sensitive details. Knowledge of elliptic curve cryptography (ECC) and hash functions like SHA-256 is essential, as they form the backbone of these proof systems. For a practical deep dive, resources like the ZKP MOOC or the book "Proofs, Arguments, and Zero-Knowledge" by Justin Thaler are highly recommended.

Your development environment requires specific tooling. You will need a Rust compiler (version 1.70+) for performance-critical circuits and a Node.js/npm setup for higher-level frameworks. The primary tool for this tutorial is Circom 2.1.6, a domain-specific language for defining arithmetic circuits, paired with snarkjs for proof generation and verification. Install them via npm: npm install -g circom snarkjs. You'll also need a trusted setup ceremony to generate proving and verification keys, which we will simulate using the Powers of Tau.

For the AML logic itself, you must define the compliance rules as a circuit. This involves translating policy checks—such as verifying a user's age is over 18, ensuring a transaction amount is below a sanctioned threshold (e.g., $10,000), or checking against a private list of flagged addresses—into arithmetic constraints. We will use a simple example: a circuit that proves a transaction value is below a limit without revealing the actual amount. This requires structuring the problem so all inputs and logic are represented within the finite field of the chosen elliptic curve, such as BN128.

Finally, you need a basic application to integrate the ZKP. This involves a backend service (using a framework like Express.js or FastAPI) to handle proof generation and a smart contract for on-chain verification. The contract will be written in Solidity 0.8.x and deployed to an EVM-compatible testnet like Sepolia. The verification key generated by snarkjs will be embedded in this contract. Ensure you have MetaMask installed and test ETH from a faucet to interact with the contract.

key-concepts-text
CORE CONCEPTS: ZKPS AND AML CIRCUITS

Setting Up a Zero-Knowledge Proof System for AML Screening

A practical guide to implementing a ZKP-based system for private Anti-Money Laundering compliance checks, using Circom and the Groth16 proving scheme.

A Zero-Knowledge Proof (ZKP) system for Anti-Money Laundering (AML) screening allows a financial institution to prove a transaction complies with sanctions lists or risk rules without revealing the sensitive customer data involved. This is achieved by encoding the compliance logic into an arithmetic circuit—a set of mathematical constraints that represent the allowed operations. Popular frameworks like Circom or Noir are used to write these circuits. The core components are a prover, which generates a proof of valid execution, and a verifier, a lightweight program that checks the proof's validity using a public verification key.

To build an AML circuit, you first define the private and public inputs. Private inputs are the sensitive data to be kept secret, such as a customer's name, date of birth, or transaction amount. Public inputs are the non-sensitive parameters known to the verifier, like the current date or a cryptographic commitment to the sanctions list. The circuit's logic performs checks—for example, verifying that a hashed customer identifier is not present in a Merkle tree of sanctioned entities. The output is a single boolean signal indicating a pass or fail, which becomes part of the public proof.

Here is a simplified example of a Circom circuit that checks if a private value is not zero (a basic compliance rule). This circuit, named AMLBasicCheck, takes a private signal privateValue and outputs a public signal isCompliant.

circom
pragma circom 2.0.0;
template AMLBasicCheck() {
    // Private input: the value to check (e.g., risk score)
    signal input privateValue;
    // Public output: 1 for compliant (non-zero), 0 for non-compliant
    signal output isCompliant;
    // Constraint: isCompliant is 1 if privateValue != 0
    isCompliant <== 1 - (privateValue == 0);
}
component main = AMLBasicCheck();

After compiling this circuit, you use a trusted setup ceremony (like Phase 2 of Perpetual Powers of Tau) to generate the proving and verification keys required for the Groth16 zk-SNARK protocol.

The proving process involves a backend service using the proving key, the private inputs (the actual customer data), and the public inputs to generate a cryptographic proof. This proof is then sent on-chain or to a regulator alongside the public output (isCompliant: 1). The verifier, which could be a smart contract on Ethereum using the snarkjs library, only needs the proof, the public inputs, and the verification key to confirm the statement's truth. This entire flow ensures selective disclosure: the verifier learns the result of the AML check but gains zero knowledge about the underlying private data that led to it.

For production systems, circuits must handle complex real-world logic. This includes checking against dynamic, updatable sanctions lists using data structures like Merkle trees or vector commitments, and implementing risk scoring models with privacy-preserving comparisons. Major challenges include managing the trusted setup, ensuring circuit efficiency to minimize prover costs and time, and maintaining the integrity of the public reference data. Projects like Tornado Cash (for transaction privacy) and Aztec Network (for private smart contracts) demonstrate the practical application of these principles at scale.

To get started, set up a development environment with Circom and snarkjs. Define your AML policy as a set of constraints, implement the circuit, and run it through the full workflow: compilation, trusted setup, proof generation, and verification. Testing with sample data is critical before integrating the prover into a backend service and deploying the verifier contract. This setup moves compliance from a data-sharing model to a proof-of-compliance model, enhancing user privacy while meeting regulatory obligations.

step1-circuit-design
CIRCUIT LOGIC

Step 1: Design the AML Verification Circuit

This step defines the core logic for verifying Anti-Money Laundering (AML) compliance using zero-knowledge proofs, creating a private, verifiable check.

An AML verification circuit is a zero-knowledge circuit that proves a user's transaction or wallet address is not on a sanctions list, without revealing the address itself. The circuit takes private inputs (the user's address) and public inputs (a cryptographic commitment to the sanctions list). Its job is to output a proof that the private address is not contained within the committed list. This proof can be verified by any third party, confirming compliance while preserving user privacy. We typically implement this using a zk-SNARK framework like Circom or Halo2.

The core logic involves a Merkle tree inclusion proof. The sanctioned addresses are hashed and stored in a Merkle tree. The circuit's public input is the Merkle root, which commits to the entire list. The private inputs are: the user's address, a Merkle proof (sibling hashes), and the index proving their address is not in the tree. The circuit hashes the user's address, verifies the provided Merkle proof does not lead to the committed root for that leaf, and outputs true for a valid non-membership proof. This is more efficient than checking against the entire list inside the circuit.

Here is a simplified conceptual outline in pseudo-Circom syntax for the verification logic:

code
template AMLNonMember(nLevels) {
    signal input leaf; // Private: user address hash
    signal input root; // Public: Merkle root of sanctions list
    signal input pathElements[nLevels]; // Private: sibling hashes
    signal input pathIndices[nLevels]; // Private: index bits

    // Recompute the Merkle root from the leaf and proof
    component merkleVerifier = MerkleTreeChecker(nLevels);
    merkleVerifier.leaf <== leaf;
    for (var i = 0; i < nLevels; i++) {
        merkleVerifier.pathElements[i] <== pathElements[i];
        merkleVerifier.pathIndices[i] <== pathIndices[i];
    }
    // Constraint: The computed root MUST NOT equal the public root
    // This proves the leaf is *not* in the tree.
    merkleVerifier.root !== root;
}

The key constraint merkleVerifier.root !== root enforces non-membership.

Design considerations include choosing the right hash function (like Poseidon or MiMC for zk-friendly hashing), determining the tree depth to accommodate the list size, and managing how the list commitment (root) is updated and published. The circuit must also handle edge cases, such as verifying the proof corresponds to the correct leaf position. The output is a zk-SNARK proof that can be verified on-chain by a smart contract, enabling private compliance checks for DeFi protocols or cross-chain bridges.

This design shifts the trust model. Verifiers only need to trust that the Merkle root is correct and up-to-date, which can be maintained by a reputable oracle or a decentralized committee. The user never exposes their address to the verifier or the public chain, achieving selective disclosure. This foundational circuit is the first step in building a full ZK-AML system, which can be extended to check against dynamic risk scores or multiple list types.

step2-oracle-integration
DATA FEED

Step 2: Integrate an Oracle for List Updates

Connect your ZK circuit to a secure, real-time data feed containing the latest sanctions and AML watchlists.

A zero-knowledge proof system for AML screening is only as current as its underlying data. Manually updating a list of sanctioned addresses within a circuit is impractical and insecure. An oracle solves this by acting as a secure bridge between your on-chain verifier and off-chain data sources. In this setup, a trusted oracle service (like Chainlink, Pyth, or a custom provider) periodically fetches the latest watchlist data, generates a cryptographic commitment (e.g., a Merkle root), and publishes it to the blockchain. Your ZK circuit can then verify that a user's address is not part of the committed list without ever seeing the raw data.

The core technical integration involves two components: the on-chain verifier contract and the off-chain oracle service. The oracle's role is to compute a new Merkle root whenever the watchlist updates and post it to a smart contract. Your verifier contract stores this root. When a user generates a ZK proof, their circuit uses this public root as an input. The proof demonstrates that the user's private address, when hashed and checked against the Merkle tree, is not a leaf node under that root. This ensures the proof is valid only against the most recent list.

For development, you can start with a mock oracle. Here's a simplified Solidity example for an updater contract an oracle would call:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract AMLListOracle {
    address public owner;
    bytes32 public currentListRoot;
    event ListRootUpdated(bytes32 newRoot);
    constructor() { owner = msg.sender; }
    function updateListRoot(bytes32 _newRoot) external {
        require(msg.sender == owner, "Unauthorized");
        currentListRoot = _newRoot;
        emit ListRootUpdated(_newRoot);
    }
}

In production, you would replace the owner check with a more robust verification mechanism, such as a decentralized oracle network's signature.

Security considerations for the oracle are paramount. A compromised oracle providing an incorrect or stale root would invalidate all proofs. Mitigations include using decentralized oracle networks (DONs) that aggregate data from multiple nodes, implementing time-locks or challenge periods for root updates to allow for dispute resolution, and setting up monitoring alerts for failed update cycles. The cost of updating the root on-chain (gas fees) must also be factored into the oracle's economic model.

Finally, your ZK circuit (e.g., written in Circom or Halo2) must be designed to accept the Merkle root as a public input. The prover will supply the Merkle path and sibling hashes as private inputs to demonstrate non-membership. This integration creates a dynamic system where AML compliance checks are both private and automatically updated, a critical requirement for any real-world regulatory application.

step3-generate-proofs
IMPLEMENTATION

Step 3: Generate and Verify Proofs Off-Chain

This step details the core cryptographic process of creating a zero-knowledge proof for AML compliance and verifying it locally before on-chain submission.

With the compliance circuit compiled, you can now generate a zero-knowledge proof (ZKP). This proof cryptographically demonstrates that a private transaction satisfies your AML rules—such as being under a sanction threshold or from a whitelisted jurisdiction—without revealing the sensitive input data itself. Using a proving key generated during the trusted setup, a prover (e.g., a user's wallet) runs the prove function. For a simple check, the private inputs might be the transaction amount and a country code, while the public input is a public sanction threshold. The output is a compact proof, typically a few kilobytes in size.

The proof must be verified off-chain immediately after generation. This is a critical sanity check to prevent submitting invalid proofs to the blockchain, which would waste gas and fail. Using the corresponding verification key, you run the verify function locally. This function takes the proof and the public inputs (like the threshold) and returns a boolean. A successful verification confirms the proof is valid for the given public parameters. Libraries like snarkjs for Circom or the native prover for Noir provide straightforward APIs for this step. Always implement this verification in your client-side logic.

Let's examine a conceptual code snippet using a Circom/ZK-SNARKs workflow. After compiling a circuit aml_check.circom, you would use snarkjs to generate the proof.

javascript
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
  { amount: 50, countryCode: 123 }, // Private inputs
  "aml_check_js/aml_check.wasm",   // Witness calculator
  "aml_check.zkey"                 // Proving key
);
// Off-chain verification
const isValid = await snarkjs.groth16.verify(
  "verification_key.json",
  publicSignals, // e.g., [publicThreshold]
  proof
);
console.log("Proof valid locally:", isValid); // Should be `true`

This local verification is fast and cost-free, ensuring only valid proofs proceed.

Different ZK frameworks have distinct workflows. For Noir and Aztec, you would use their native bb.js or aztec-nargo provers. For ZK-SNARKs with libsnark or bellman, the process involves generating a QAP (Quadratic Arithmetic Program) and creating a proof using elliptic curve pairings. The core principle remains: private witness data is fed into the arithmetic circuit, and the proving algorithm generates a cryptographic proof of correct execution. Understanding your stack's specific prover is essential for performance tuning and integration.

After successful local verification, the proof and the required public signals are ready for the final step: on-chain verification. The public signals are the data that will be known to and checked by the verifier contract on-chain, such as a public root hash of a whitelist Merkle tree or a maximum allowable amount. It is crucial that these public inputs are consistent between proof generation and on-chain verification. Any mismatch will cause the expensive on-chain verification to fail, rejecting the compliance claim despite a valid off-chain proof.

This off-chain stage is where the privacy guarantee is cryptographically sealed. The proof contains no information about the private inputs that could be used to reconstruct them, relying on the security of the underlying cryptographic assumptions (like the discrete log problem). By decoupling proof generation and verification, ZK systems enable complex, private compliance checks where the computational heavy lifting is done off-chain, and only a tiny, verifiable proof is posted to the immutable ledger.

step4-smart-contract-verifier
IMPLEMENTATION

Step 4: Deploy the On-Chain Verifier

This step involves compiling your ZK circuit into a verifier smart contract and deploying it to your target blockchain, enabling on-chain proof verification for your AML screening system.

With your ZK circuit finalized and tested, the next step is to generate a verifier smart contract. This contract contains the cryptographic verification key and the logic to check the validity of a submitted proof against a public statement. Using your chosen framework—such as Circom with snarkjs or Noir with nargo—you will compile the circuit's intermediate representation (like an R1CS) to produce the Solidity or Vyper code. For example, with Circom, you would run snarkjs zkey export solidityverifier circuit_final.zkey verifier.sol to generate the contract. This contract is the immutable, trustless component that will live on-chain.

Before deployment, you must thoroughly audit the generated verifier contract. Key checks include ensuring the contract correctly imports the necessary verification libraries (like Pairing.sol for Groth16), the verification function is properly exposed, and all public signals (inputs) are correctly mapped. It is also critical to verify that the contract's verification key matches the one used to generate proofs off-chain; a mismatch will cause all proofs to be rejected. Consider using tools like Slither or Mythril for a basic security analysis, and for production systems, a professional audit is highly recommended.

Deploy the verifier contract to your chosen blockchain network (e.g., Ethereum Mainnet, Arbitrum, Polygon). The deployment cost is a one-time gas fee and is primarily determined by the size and complexity of the verification key embedded in the contract. For a standard Groth16 proof, deployment can cost between 3 to 8 million gas. Use standard deployment tools like Hardhat, Foundry, or Remix. Once deployed, record the contract address—this is the on-chain endpoint your application will call to verify proofs. The contract's state is typically immutable after deployment, so ensure all parameters are correct.

After deployment, you must integrate the verifier address into your application's backend. Your proving service, after generating a proof for a user's AML check, will call the verifier contract's verifyProof function, passing the proof (a, b, c points) and the public signals. A return value of true confirms the proof is valid, meaning the user passed the screening criteria without revealing their private data. You should implement robust error handling and gas estimation for this transaction, as verification gas costs are non-trivial (often 200k-500k gas) and must be accounted for in your transaction flow.

Finally, establish a monitoring and maintenance protocol. Log all verification transactions, track gas usage, and set up alerts for failed verifications. Since the cryptographic trust assumptions are tied to the initial trusted setup, document the setup ceremony details (participants, transcript) for transparency. The on-chain verifier is a long-lived component; plan for potential upgrades by designing a system where a new verifier contract can be deployed and your application's reference to it can be updated via a governance mechanism or admin function, ensuring your AML system can evolve.

COMPARISON

ZKP AML vs. Traditional Screening

Key differences between privacy-preserving ZKP-based AML screening and conventional centralized methods.

Feature / MetricZKP-Based AML ScreeningTraditional AML Screening

Data Privacy

Regulatory Compliance

False Positive Rate

0.1% - 0.5%

2% - 5%

Screening Latency

< 1 second

2 - 10 seconds

Audit Trail Transparency

Cryptographically verifiable

Opaque, internal logs

Cross-Border Data Sharing

Permissionless, no data transfer

Requires legal agreements

Operational Cost per 1M Checks

$10 - $50

$500 - $2000

Resistance to Front-Running

ZK AML IMPLEMENTATION

Frequently Asked Questions

Common technical questions and solutions for developers building zero-knowledge proof systems for Anti-Money Laundering (AML) screening.

A ZK-SNARK (Zero-Knowledge Succinct Non-Interactive Argument of Knowledge) is a cryptographic proof system. It allows one party (the prover) to prove to another (the verifier) that a statement is true without revealing the underlying data.

For AML screening, this enables a user to prove their transaction complies with regulations—like not interacting with a sanctioned address—without exposing their entire transaction history or the specific addresses they are checking against. The core components are:

  • Arithmetic Circuit: The AML rule (e.g., "sender not on OFAC list") is compiled into a circuit.
  • Trusted Setup: A one-time ceremony generates public parameters (proving and verification keys).
  • Proof Generation: The prover uses the proving key to create a small proof.
  • Proof Verification: The verifier checks the proof against the verification key and public inputs.

Frameworks like Circom or ZoKrates are used to write these circuits. The proof is succinct (small) and verification is fast, making it suitable for blockchain applications.

conclusion
IMPLEMENTATION SUMMARY

Conclusion and Next Steps

This guide has walked through the core components of building a zero-knowledge proof system for AML screening, from circuit design to verification. Here are the key takeaways and directions for further development.

You have now implemented a foundational zero-knowledge AML screening system. The core workflow involves a user proving they are not on a sanctions list without revealing their identity, and a verifier checking this proof. This is achieved by constructing a zk-SNARK circuit (e.g., using Circom) that hashes a private input, checks it against a public Merkle root of a banned list, and outputs 0 for a match (sanctioned) or 1 for a non-match (clear). The prover generates a proof using tools like snarkjs, which the verifier can validate with minimal computational effort.

For production deployment, several critical next steps are required. First, integrate a secure and updatable mechanism for the Merkle tree containing the sanctions list, potentially using a decentralized oracle or a trusted off-chain service with on-chain root commits. Second, implement robust key management for the prover's private identity seed, considering hardware security modules or MPC wallets. Third, design the system's trust model: will you use a trusted setup (requiring a secure ceremony) or explore transparent setups like STARKs or Halo2? Each choice involves trade-offs between proof size, verification speed, and trust assumptions.

To extend the system's capabilities, consider these advanced implementations. You could build privacy-preserving risk scoring by allowing users to prove multiple attributes (e.g., transaction history under a threshold) satisfy a policy. Integrating with zkRollups like zkSync or StarkNet can batch thousands of screening proofs for efficient on-chain settlement. Furthermore, explore recursive proofs to aggregate screenings over time, creating a persistent, private compliance record. Always audit your circuits with tools like Picus and zkSecurity and benchmark gas costs for on-chain verification.

The code and concepts here provide a template. The field of applied zk cryptography is rapidly evolving. To continue learning, study production systems like Tornado Cash for anonymity set design, Aztec Network for private smart contracts, and Semaphore for anonymous signaling. Engage with the community through the ZKProof Standardization efforts and the documentation for leading frameworks such as Circom, Halo2, and Noir. Building a secure, private AML system is a complex but solvable engineering challenge at the frontier of blockchain technology.