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

How to Implement a Proof-of-Reserves Verification System

This guide provides a step-by-step technical walkthrough for building a cryptographically verifiable proof-of-reserves system, covering Merkle tree construction, on-chain commitments, and privacy-preserving liability proofs.
Chainscore © 2026
introduction
TECHNICAL GUIDE

Introduction to Proof-of-Reserves Verification

A practical guide to implementing a cryptographic system that proves a custodian holds sufficient assets to cover its liabilities.

A Proof-of-Reserves (PoR) system cryptographically verifies that a financial custodian, like an exchange or lending protocol, holds the assets it claims to hold for its users. It addresses the core question of solvency without revealing sensitive internal data. The typical architecture involves three main components: a Merkle tree of user balances, a cryptographic attestation from the custodian, and a public verification process. This creates a trust-minimized audit where users can independently confirm their funds are included in the proven total reserve.

The process begins with the custodian generating a snapshot of all user account balances at a specific block height. Each user's balance and a unique identifier (like a user ID or nonce) are hashed to create a leaf node. These leaves are then aggregated into a Merkle tree, whose root hash becomes a succinct, tamper-proof commitment to the entire set of liabilities. Crucially, the custodian must also provide a cryptographic signature or attestation—often via a zero-knowledge proof (ZKP) or a signed message from a verifiable wallet—proving control over the wallets holding the reserve assets on-chain.

For a robust implementation, the reserve attestation must be asset-specific and timestamped. For example, proving custody of Bitcoin requires signing a message from the Bitcoin addresses holding the reserves. For Ethereum and EVM-based assets, this often involves signing a message with the reserve wallet's private key. The attestation data—including the total reserve value, the blockchain addresses, and the signature—is published alongside the Merkle root. This allows anyone to verify that the signed reserves match or exceed the total liabilities committed in the Merkle tree.

Users verify their inclusion via a Merkle proof. The custodian provides each user with their hashed leaf data and the sibling hashes needed to reconstruct the path to the published root. A user can run a simple function to hash their provided data and verify it matches the public root. Tools like the MerkleTreeJS library facilitate this. For example, a basic verification in JavaScript would use merkleTree.verify(proof, leaf, root). This proves their balance was correctly included in the proven total, though it does not reveal other users' data.

Advanced implementations use zero-knowledge proofs to enhance privacy and security. A zk-SNARK can prove that the sum of all leaf balances in the Merkle tree equals the total liabilities, and that this sum is less than or equal to the proven reserves, without revealing individual balances. Protocols like zkSync and Aztec have pioneered such privacy-preserving proofs. This approach mitigates the privacy leakage of a standard Merkle tree and prevents inference of a platform's total user count or wealth distribution.

Effective PoR requires regular, scheduled attestations (e.g., monthly) and should be auditable on-chain. Best practices include using a multi-signature or smart contract wallet for reserves to prevent single-point failure, publishing all code and methodologies as open-source, and engaging third-party auditors. The ultimate goal is to move beyond periodic proofs to continuous, real-time verification via validators or light clients, creating a transparent and verifiable standard for custodial trust in Web3.

prerequisites
IMPLEMENTATION GUIDE

Prerequisites and System Requirements

Before building a Proof-of-Reserves (PoR) verification system, you need the right technical foundation. This guide outlines the essential knowledge, tools, and infrastructure required.

A Proof-of-Reserves system cryptographically proves a custodian holds sufficient assets to cover client liabilities. Core prerequisites include a strong understanding of cryptographic primitives like Merkle trees for aggregating balances, digital signatures (e.g., ECDSA with secp256k1) for attestations, and hash functions (SHA-256). You must also be familiar with the target blockchain's data structures—how to query wallet balances via RPC calls to nodes (using providers like Infura, Alchemy, or a local Geth/Erigon instance) and parse transaction histories.

Your development environment needs specific tooling. For Ethereum and EVM chains, proficiency with web3.js or Ethers.js libraries is essential for interacting with the blockchain. A backend service (Node.js, Python, Go) will handle the Merkle tree construction and API serving. You'll also need access to secure key management solutions (HSMs, cloud KMS like AWS KMS or GCP Cloud KMS) for generating and storing the auditor's signing keys, as the system's trust hinges on this private key's security.

System requirements depend on scale. For a centralized exchange verifying millions of accounts, the process is resource-intensive. You need a server with sufficient RAM and CPU to build a massive Merkle tree—a tree with 1 million leaves can require several GB of memory during construction. The system must schedule periodic balance snapshots, which involves querying potentially thousands of addresses across multiple blockchains, requiring reliable, high-throughput RPC endpoints and robust error handling for chain reorganizations.

Data integrity is paramount. The verification system must fetch balance data at a specific, immutable block height. You cannot rely on the latest block due to chain re-orgs. The published proof must include this block number and a timestamp. Furthermore, you need a mechanism to publish the proof's root hash and auditor signature on-chain (e.g., via a simple smart contract) or in a transparent public repository like GitHub or IPFS, ensuring users can independently verify the data's authenticity and timestamp.

Finally, consider the user verification client. While the core system generates the proof, you should provide open-source tools for users to verify their inclusion. This typically involves a static web page or a CLI tool that allows a user to input their account ID and balance, then cryptographically verifies their leaf's path against the published Merkle root and auditor signature, completing the trust loop without relying on the custodian's live systems.

key-concepts
PROOF-OF-RESERVES

Core Cryptographic Concepts

A Proof-of-Reserves (PoR) system cryptographically verifies that a custodian holds sufficient assets to cover all client liabilities. This guide covers the core cryptographic components required to build a verifiable system.

01

Merkle Trees for Liability Proofs

A Merkle tree is the standard data structure for aggregating user account data into a single, verifiable commitment (the root hash).

  • Leaf nodes represent individual user balances and identifiers, hashed together.
  • Non-inclusion proofs can demonstrate a user is not in the tree, preventing fake liability inflation.
  • Sparse Merkle Trees are often used for efficient updates and non-membership proofs.

This structure allows any user to verify their inclusion in the total liabilities without revealing other users' data.

02

Digital Signatures for Attestation

A custodian's attestation to the reserve and liability states must be cryptographically signed to be trustworthy.

  • The reserve attestation (e.g., on-chain transaction or signed message from a custody address) proves asset ownership.
  • The liability attestation is a signed Merkle root, committing to all user balances at a specific block height.
  • Use of multi-signature schemes or threshold signatures from independent auditors increases trust by distributing signing power.

Verifiers check these signatures against known auditor or custodian public keys.

03

Zero-Knowledge Proofs for Privacy

Zero-Knowledge Proofs (ZKPs), like zk-SNARKs or zk-STARKs, enable privacy-preserving PoR.

  • A ZKP can prove that the total liabilities in a Merkle tree are less than or equal to the proven reserves without revealing individual balances.
  • This allows for selective disclosure: users can verify their own inclusion, while the aggregate data remains confidential.
  • Systems like zkRollups use similar technology to prove state transitions, which can be adapted for reserve verification.

Implementing ZKPs adds computational overhead but maximizes user privacy.

04

On-Chain vs. Off-Chain Verification

PoR systems differ in where verification logic and data are stored.

  • On-Chain Verification: Smart contracts (e.g., on Ethereum) store the Merkle root and verify user inclusion proofs. Reserves are proven via on-chain asset holdings. This is fully transparent and trust-minimized.
  • Off-Chain Verification: The custodian publishes signed attestations (Merkle root, reserve snapshot) to a public server. Users verify proofs locally using open-source tools. This is more flexible but requires trust in data availability.

Hybrid approaches use on-chain roots for censorship resistance with off-chain proof generation.

05

Time-Based Commitments and Fraud Proofs

A robust PoR system must prevent data manipulation between attestations.

  • Commit-Reveal Schemes: The custodian commits to a future state (e.g., hashes of the upcoming Merkle root). This prevents retroactive changes to the liability set.
  • Fraud Proofs: Allow any third party to cryptographically challenge an invalid attestation. If a user can provide a valid Merkle proof that contradicts the published root, the fraud is proven.
  • Frequent Attestations: Regular proofs (e.g., daily) reduce the window for malicious activity. The Proof of Solvency paper by Binance details this model.

These mechanisms enforce consistency over time.

step-1-snapshot
DATA COLLECTION

Step 1: Taking a Balance Snapshot

The first technical step in a Proof-of-Reserves (PoR) system is to cryptographically capture the total liabilities of the protocol at a specific point in time.

A balance snapshot is a verifiable record of all user account balances on a specific block. This aggregated sum represents the protocol's total liabilities—the amount it owes its users. The critical requirement is that this snapshot must be immutable and cryptographically provable. You cannot simply query a database and publish a number; you must generate a cryptographic commitment to the data, such as a Merkle root, that anyone can independently verify against the blockchain's history. This prevents the protocol from manipulating the data after the fact.

To generate the snapshot, you must iterate over all user accounts in your system. For each account, you need its address and its balance at the target block height. This data is typically sourced from your protocol's on-chain smart contracts or indexed off-chain state. The output is a list of (address, balance) pairs. It is crucial to use the exact block number for consistency, as balances can change between blocks. This block number becomes the snapshot's timestamp and a key input for the subsequent verification proof.

The most common method for committing to this data is to construct a Merkle tree (specifically, a Merkle sum tree). Each leaf node is a hash of keccak256(abi.encodePacked(address, balance)). The tree is built by recursively hashing pairs of leaves until a single root hash remains. This Merkle root is your snapshot's fingerprint. Publishing this root, along with the block number, constitutes the proof of liabilities. The security property is that it is computationally infeasible to create a different set of balances that produces the same root.

Here is a simplified conceptual example of the snapshot generation logic in Solidity-style pseudocode:

code
// At a specific block number `snapshotBlock`
MerkleTree tree = new MerkleTree();

for (UserAccount account in allAccounts) {
    bytes32 leaf = keccak256(abi.encodePacked(account.address, account.getBalanceAtBlock(snapshotBlock)));
    tree.addLeaf(leaf);
}

bytes32 merkleRoot = tree.getRoot();
// Publish: merkleRoot and snapshotBlock

In practice, you would use a library like OpenZeppelin's MerkleProof for verification and a dedicated off-chain script for tree generation to avoid gas costs.

After publishing the Merkle root, you must provide users with the tools to verify their inclusion. This is done by supplying each user with their Merkle proof—the sequence of sibling hashes needed to reconstruct the path from their leaf to the published root. A user can then run a verification function, providing their address, balance, and the proof, to confirm their funds are included in the total liabilities. This process, from data collection to proof distribution, establishes the foundational trust layer for the entire Proof-of-Reserves system.

step-2-merkle-tree
DATA STRUCTURE

Step 2: Constructing the Merkle Tree

A Merkle tree is the cryptographic backbone of a Proof-of-Reserves system, enabling efficient and verifiable aggregation of user balances.

A Merkle tree (or hash tree) is a data structure where every leaf node is the cryptographic hash of a piece of data, and every non-leaf node is the hash of its child nodes. For Proof-of-Reserves, each leaf represents a user's account data, typically hashed from a structured string like address:balance. The final, single hash at the root (Merkle root) is a unique fingerprint of the entire dataset. This root is what gets published on-chain, serving as a public commitment to the exchange's claimed liabilities.

To construct the tree, you first need to prepare the leaf data. For each user, create a deterministic string. A common format is {address}:{balance}, where the balance is in the smallest unit (wei, satoshis) to avoid decimals. For enhanced privacy and security, you can include a random nonce to prevent rainbow table attacks, resulting in a leaf string like {address}:{balance}:{nonce}. Each of these strings is then hashed using a secure algorithm like SHA-256 or Keccak-256 to generate the leaf node hashes.

With all leaf hashes computed, the tree is built from the bottom up. If the number of leaves isn't a power of two, empty leaves (often hashes of zero) are added to pad the tree. Pairs of leaf hashes are concatenated and hashed to create their parent node. This process repeats recursively: parent_hash = hash(child1_hash + child2_hash). Libraries like merkletreejs in JavaScript or pymerkle in Python automate this process. The critical output is the Merkle root, a 32-byte hash that summarizes every user's balance.

The integrity of the system relies on the properties of cryptographic hash functions. Any change to a single user's balance, or the addition/removal of a user, will produce a completely different Merkle root. This makes it impossible for an exchange to silently alter the dataset after publishing the root. The root's immutability, once stored on a blockchain like Ethereum or published in a transparency report, becomes the anchor point for all subsequent verification.

For developers, here's a simplified code snippet using merkletreejs and keccak256:

javascript
const { MerkleTree } = require('merkletreejs');
const keccak256 = require('keccak256');

// 1. Create leaf data
const leaves = [
  '0xabc...:1000000000000000000:12345',
  '0xdef...:2500000000000000000:67890'
].map(x => keccak256(x));

// 2. Build tree
const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });

// 3. Get the Merkle Root
const root = tree.getRoot().toString('hex');
console.log('Merkle Root:', root);

This root is your system's core commitment.

Finally, the construction process must be reproducible and auditable. The exchange must document and open-source the exact methodology: the leaf format, hash function, concatenation order, and padding rules. Without this specification, users cannot independently verify their inclusion. The published root is meaningless without the public ability to reconstruct the same tree from the underlying data, which is enabled by providing cryptographic Merkle proofs to each user, which we will cover in the next step.

step-3-onchain-commitment
ON-CHAIN VERIFICATION

Step 3: Publishing the On-Chain Commitment

This step involves publishing a cryptographic commitment to your asset data on-chain, creating a public, tamper-proof anchor for the verification process.

The on-chain commitment is the core cryptographic proof in a Proof-of-Reserves system. It is a single hash, such as a Merkle root or a zk-SNARK public input, that represents the entire dataset of user balances and the total reserve amount. Publishing this hash to a public blockchain, like Ethereum or Solana, creates an immutable and timestamped record. This serves as the definitive reference point that any verifier can use to check the validity of your off-chain proof data. The transaction must be signed by the custodian's verifiable wallet address to establish authorship.

The specific implementation depends on your chosen cryptographic method. For a Merkle tree approach, you publish the root hash. A common pattern is to use a simple smart contract with a function like publishRoot(bytes32 merkleRoot, uint256 totalReserves). For a zk-SNARK (Zero-Knowledge Succinct Non-Interactive Argument of Knowledge), you publish the public inputs to the verification contract, which typically include the commitment hash and the total reserves. The actual proof is submitted separately for verification against these inputs. This step does not reveal any individual user data.

Critical best practices must be followed. The published total reserve value must match the sum of all user balances in your committed dataset. The transaction should include a clear event emission (e.g., RootPublished) for easy indexing by explorers and auditors. Always use a time-lock or similar mechanism if publishing on a schedule; the commitment should be published after the data snapshot is taken but before the proof is made available, preventing manipulation. Record the transaction hash and block number as they are essential for the verification link.

Here is a simplified example of a Solidity function for publishing a Merkle root:

solidity
event RootPublished(bytes32 indexed merkleRoot, uint256 totalReserves, uint256 blockNumber);
bytes32 public latestMerkleRoot;
uint256 public latestTotalReserves;

function publishReservesCommitment(bytes32 _merkleRoot, uint256 _totalReserves) external onlyOwner {
    latestMerkleRoot = _merkleRoot;
    latestTotalReserves = _totalReserves;
    emit RootPublished(_merkleRoot, _totalReserves, block.number);
}

This contract stores the latest commitment and emits an event, allowing anyone to track the history.

After successful publication, the on-chain commitment becomes the source of truth. The next step involves generating the corresponding off-chain proof data (like Merkle proofs for each user) and making it publicly accessible, typically via an API. The combination of the immutable on-chain hash and the available off-chain data enables users and independent auditors to perform verification. Without this published commitment, there is no cryptographic anchor against which to verify the custodian's claims, rendering any off-chain data unverifiable.

step-4-liability-proofs
IMPLEMENTATION GUIDE

Step 4: Proving Liabilities and Solvency

This guide details the technical implementation of a Proof-of-Reserves (PoR) verification system, moving from theory to practical code.

A Proof-of-Reserves verification system cryptographically proves that a custodian holds sufficient assets to cover all user liabilities. The core components are a Merkle tree of user liabilities and a digital signature over the total assets. The Merkle root commits to all user balances at a specific block height, while the signature, created with the custodian's private key, attests to the total reserve value. This creates a tamper-proof, publicly verifiable snapshot. For a real-world example, review the methodology used by exchanges like Kraken or Binance in their published attestation reports.

The implementation begins by constructing the Merkle tree of liabilities. For each user account, you must create a leaf node. A common and secure pattern is to hash the concatenation of the user's identifier (like an Ethereum address) and their balance. In Solidity, this can be represented as leaf = keccak256(abi.encodePacked(userAddress, balance)). It is critical to use a cryptographically secure hash function and to ensure the data encoding is deterministic and consistent for all verifiers. The final Merkle root is the single hash that represents the entire set of user obligations.

Next, you must generate the proof of assets. This involves the custodian cryptographically signing a message containing the total reserve value and the Merkle root. A standard format is to sign keccak256("\x19Ethereum Signed Message:\n32", keccak256(abi.encodePacked(totalAssets, merkleRoot, blockNumber))). This signature, when verified against the custodian's known public address, proves they attested to those specific numbers. The blockNumber is included to timestamp the attestation and prevent replay attacks using outdated data.

The verification process is performed by any third party, such as a user or an auditor. Given the published Merkle root, total assets, block number, custodian's signature, and a Merkle proof for their specific balance, a user can verify two things: 1) That their balance is correctly included in the published liabilities (via the Merkle proof), and 2) That the custodian signed off on the total assets being equal to or greater than the sum of those liabilities. A simple smart contract can automate this verification, increasing trust through decentralization.

While a basic PoR proves custody at a point in time, it has limitations. It does not prove the ownership of liabilities—only their inclusion. A malicious custodian could exclude accounts from the tree. Proof-of-Liabilities schemes, like those proposed by the zk-proof community, aim to prove the sum of all leaves equals the total liabilities without revealing individual balances. Furthermore, PoR is a snapshot, not a real-time guarantee. For continuous assurance, consider implementing frequent, automated attestations or integrating with on-chain verification oracles.

privacy-zkp
ADVANCED: PRIVACY WITH ZERO-KNOWLEDGE PROOFS

How to Implement a Proof-of-Reserves Verification System

A technical guide for developers on building a privacy-preserving proof-of-reserves system using zero-knowledge proofs to verify asset holdings without revealing sensitive data.

A proof-of-reserves (PoR) system cryptographically verifies that a custodian, like an exchange, holds sufficient assets to cover all client liabilities. Traditional audits require sharing sensitive user data with a third party. By implementing zero-knowledge proofs (ZKPs), you can prove solvency while maintaining user privacy and operational security. This guide outlines the core architecture and steps to build such a system using modern ZK tooling like Circom for circuit design and SnarkJS for proof generation and verification.

The system's foundation is a Merkle tree where each leaf is a commitment to a user's balance and identity. The root of this tree, published on-chain, represents the total proven liabilities. To generate a proof, the prover (the exchange) must demonstrate two things without revealing individual leaf data: 1) Knowledge of a valid Merkle path for a specific user, and 2) That the sum of all committed balances in the tree equals the total liabilities claimed. This is achieved by constructing a ZK circuit that takes private inputs (user balances, Merkle paths, secret salts) and public inputs (Merkle root, total liability) and outputs true only if all constraints are satisfied.

Here is a simplified conceptual outline of a Circom circuit for PoR verification. The circuit would include templates for a Merkle tree inclusion proof and a running sum accumulator.

circom
// Pseudo-code for core circuit logic
template PorVerifier(nLevels) {
    signal input privateLeaf; // Hash(userId, balance, salt)
    signal input privatePathElements[nLevels];
    signal input privatePathIndices[nLevels];
    signal input privateBalance;
    
    signal output publicRoot;
    signal input publicTotalLiabilities;
    
    // Component to verify Merkle inclusion
    component merkleVerifier = MerkleInclusionProof(nLevels);
    merkleVerifier.leaf <== privateLeaf;
    // ... connect path elements and indices
    publicRoot <== merkleVerifier.root;
    
    // Component to accumulate balances (simplified)
    // In a full system, this would aggregate all leaves
    component balanceCheck = IsNonNegative();
    balanceCheck.in <== privateBalance;
    // Constraint: sum of all private balances == publicTotalLiabilities
}

The actual implementation requires a circuit that iterates over all user commitments, which is complex and resource-intensive.

After compiling the circuit, you use SnarkJS to generate the proving and verification keys. The backend service must generate a witness for each audit cycle, containing all private user data, and then create a zk-SNARK proof. This proof and the public inputs (the latest Merkle root and total liabilities) are published. Anyone can verify the proof using the on-chain verifier contract. Key steps are: 1) Data Commitment: Periodically hash user (id, balance, salt) into leaves and compute the Merkle root. 2) Proof Generation: Run the trusted setup and generate the SNARK proof off-chain. 3) On-Chain Verification: Call the verifyProof() function on the verifier contract (e.g., on Ethereum).

For production, consider using incremental Merkle trees (like those in Tornado Cash) for efficient updates and Plonk or Groth16 proving systems for performance. Major challenges include the computational cost of generating proofs for large user bases and managing the trusted setup ceremony. Frameworks like zkEVM or Noir may offer higher-level abstractions in the future. This system enhances trust by allowing continuous, privacy-preserving audits, moving beyond manual, periodic reports to a model of real-time verifiable solvency.

TECHNICAL COMPARISON

Proof-of-Reserves Implementation Methods

A comparison of core technical approaches for building a proof-of-reserves system.

Implementation FeatureMerkle Tree (Standard)Zero-Knowledge Proofs (ZK-SNARKs)On-Chain Attestation (Smart Contract)

Primary Data Structure

Merkle Patricia Trie

ZK-SNARK Circuit

On-Chain State Variable

Privacy for User Balances

Verification Cost for User

Gas fee for Merkle proof

~$0.05 - $0.30 (Prover cost)

Gas fee for view function

Auditor Computational Load

Low (Hash verification)

High (Trusted setup, proof generation)

Low (Contract state inspection)

Real-Time Update Feasibility

Low (Batch updates)

Low (Proof generation time)

High (Direct state change)

Reserve Asset Flexibility

Any on-chain asset

Primarily token balances

Any on-chain asset

Typical Audit Frequency

Daily or weekly

Weekly or on-demand

Continuous (on-chain)

Implementation Complexity

Medium

High

Low to Medium

PROOF-OF-RESERVES

Common Implementation Mistakes and Pitfalls

Implementing a Proof-of-Reserves (PoR) system requires precise cryptographic and engineering rigor. These FAQs address common developer errors that can compromise audit integrity and user trust.

This is often caused by inconsistent leaf node construction. The leaf must be a deterministic hash of the user's data, typically keccak256(abi.encodePacked(address, balance)). Common mistakes include:

  • Using abi.encode instead of abi.encodePacked, which adds padding and changes the hash.
  • Hashing non-standardized data (e.g., including a username). All leaves must follow the same schema.
  • Incorrect endianness or string formatting for addresses or balances.

Fix: Standardize the leaf-generation function off-chain and provide a public verifier script. For Ethereum, use:

solidity
bytes32 leaf = keccak256(abi.encodePacked(userAddress, balance));
DEVELOPER GUIDE

Proof-of-Reserves Implementation FAQ

Answers to common technical questions and troubleshooting steps for building a secure, verifiable Proof-of-Reserves system.

A Merkle tree (or hash tree) is a cryptographic data structure that efficiently summarizes and verifies large datasets. In Proof-of-Reserves, it's used to prove the inclusion of individual user balances within a total reserve without revealing the entire dataset.

How it works:

  • Each leaf node is a hash of a user's account ID and balance.
  • Parent nodes are hashes of their child nodes, recursively building up to a single Merkle root.
  • To prove a user's balance is included, the prover provides a Merkle proof—the minimal set of sibling hashes needed to recompute the root.

This allows any verifier to cryptographically confirm that a specific balance was part of the attested total reserves, ensuring data integrity and enabling privacy for other users' data.