Merkle Commitments
The merkle/ module provides Ligero with a compact column commitment scheme: SHA-256-hashed leaves organized in a binary tree, with compressed proofs for multi-index openings. The MerkleCommitment wrapper salts each leaf with a per-leaf nonce, making the commitment zero-knowledge. You will almost certainly not call this module directly, but this is the reference to understand what the root inside a ZkProof actually represents and how to debug opening failures.
When to reach for it
- You’re debugging a verification failure and want to isolate a Merkle-path check.
- You’re reading the proof blob format and need to know how
MerkleProofdeserializes.
Design overview
The module provides two primary classes: MerkleTree on the prover side builds a binary tree by hashing upward from n leaves stored at indices [0, n); MerkleTreeVerifier on the verifier side checks a compressed proof of inclusion for a queried set of positions.
Hashing uses SHA-256 via util/crypto.h. Internal nodes combine their children via Digest::hash2(left, right), which sequentially updates a SHA-256 context with both digests and produces the parent hash.
Compressed multi-opening. When a verifier queries a set of positions that share ancestor nodes in the tree, those ancestors are omitted from the proof blob—they’re recomputable from the queried leaves themselves. For k independent positions in a tree of n leaves, worst case is O(k · log n), but in practice much less when queries cluster (which they do in Ligero, where queried column indices are uniformly random distinct indices drawn via ts.choose()).
Zero-knowledge via leaf salting. MerkleCommitment—the wrapper Ligero uses—sets each leaf to SHA-256(nonce ‖ leaf-data) with a fresh 32-byte nonce per leaf. Nonces are revealed as part of an opening; everything else about the tableau remains hidden, preserving witness privacy.
API surface
Prover side (MerkleTree):
MerkleTree(size_t n)— allocate space fornleavesvoid set_leaf(size_t pos, const Digest& leaf)— store a leaf at positionposin the range[0, n)Digest build_tree()— compute all internal nodes bottom-up and return the rootsize_t generate_compressed_proof(std::vector<Digest>& proof, const size_t pos[], size_t np)— generate a compressed inclusion proof fornpleaf positions
Verifier side (MerkleTreeVerifier):
MerkleTreeVerifier(size_t n, const Digest& root)— initialize with tree size and the commitment rootbool verify_compressed_proof(const Digest* proof, size_t proof_len, const Digest leaves[], const size_t pos[], size_t np) const— check the compressed proof against the provided leaf values and their positions
Commitment wrapper (MerkleCommitment):
MerkleCommitment(size_t n)— allocate fornleaves with nonce vectorsDigest commit(const std::function<void(size_t, SHA256 &)> &updhash, RandomEngine &rng)— generate fresh nonces, combine each with leaf data via the providedupdhashcallback, set leaves, and return the rootvoid open(MerkleProof &proof, const size_t pos[], size_t np)— generate an opening containing the nonces and compressed path for the queried positions
Example
MerkleTree mt(4);
Digest leaves[4] = {Digest{100}, Digest{101}, Digest{102}, Digest{103}};
for (size_t i = 0; i < 4; i++) {
mt.set_leaf(i, leaves[i]);
}
Digest root = mt.build_tree();
size_t pos[2] = {1, 3};
std::vector<Digest> proof;
mt.generate_compressed_proof(proof, pos, 2);
MerkleTreeVerifier v(4, root);
Digest open_leaves[2] = {leaves[1], leaves[3]};
bool ok = v.verify_compressed_proof(proof.data(), proof.size(),
open_leaves, pos, 2);See it used
- Full test suite:
merkle_tree_test.cc - Integration in Ligero prover:
ligero_prover.h
Related
- Ligero — the primary consumer of
MerkleCommitment - Transcript & Randomness — the Merkle root is appended to the transcript