Merkle Trees in Blockchain: From Bitcoin to Airdrops
If you've ever debugged a database query, you've felt the pain of verifying data at scale. You run a SELECT query, get back 10,000 rows, and think: "How do I know this is complete and unchanged?" In traditional systems, you trust the database server to give you the right answer. Merkle trees solve this problem without a trusted middleman.
The Web2 Analogy: Hash-Based Verification
Imagine you're building an API that serves file checksums to millions of clients. You could hash each file individually, but that's inefficient—with 1 million files, you're computing 1 million hashes per request. Instead, you build a tree structure where you hash pairs of files together, then hash those results together, until you get a single root hash. Now you only need to return one hash to represent all 1 million files. This is the Merkle tree: a binary tree where every leaf is a data hash, and every parent node is the hash of its two children.
Why This Matters in Blockchain
Bitcoin's block structure relies entirely on Merkle trees. Each block contains ~2,000 transactions, and instead of storing 2,000 transaction hashes, Bitcoin only stores one: the Merkle root. This was genius for 2009 when storage was precious, and it's still foundational for SPV (Simplified Payment Verification) wallets—lightweight clients that verify transactions without downloading the entire blockchain. The key insight: you can prove a specific transaction exists in a block with only ~12 hashes, not the entire block.
How It Actually Works
Let's say you have 8 transactions: Tx1, Tx2, Tx3, Tx4, Tx5, Tx6, Tx7, Tx8.
Root
/ \
H(AB) H(CD)
/ \ / \
H1 H2 H3 H4
/ \ / \ / \ / \
T1 T2 T3 T4 T5 T6 T7 T8
To prove Tx1 exists in this tree, you need the hash of Tx1 itself, H2 (hash of Tx3+Tx4), and H(CD) (hash of all transactions 5-8). That's 3 hashes to prove 1 transaction among 8. With 2,048 transactions (a realistic block), it's ~11 hashes. This is the Merkle proof or Merkle path.
Web2 Equivalent: Merkle Proofs vs. Digital Signatures
In Web2, you might verify data integrity using HMAC-SHA256—the server computes an HMAC and sends it alongside the data, then the client recomputes it to verify nothing changed. The problem: the client still has to process all the data. With Merkle proofs, the client processes only the leaf it cares about, plus the path to the root. It's like asking a database: "Prove this customer exists in the customer table" without requiring the customer to download all customers.
Real-World: Uniswap's Airdrop
Uniswap distributed 400 million UNI tokens in September 2020. They couldn't store 250,000 claims on-chain—that would cost millions in gas. So they used a Merkle tree instead. Uniswap computed a Merkle tree of all eligible addresses and amounts, posted the root on-chain (32 bytes), and claimers submitted their address + amount + Merkle proof. The contract verified the proof against the root. This cost roughly 200k gas per claim—affordable and scalable.
Solidity Implementation
Here's a real Merkle claim contract, simplified:
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
contract MerkleAirdrop {
bytes32 public merkleRoot;
mapping(address => bool) public claimed;
event Claimed(address indexed claimer, uint256 amount);
constructor(bytes32 _merkleRoot) {
merkleRoot = _merkleRoot;
}
function claim(uint256 amount, bytes32[] calldata proof) external {
require(!claimed[msg.sender], "Already claimed");
// Compute the leaf hash
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, amount));
// Verify the proof
require(
MerkleProof.verify(proof, merkleRoot, leaf),
"Invalid proof"
);
claimed[msg.sender] = true;
// Transfer tokens...
emit Claimed(msg.sender, amount);
}
}
The MerkleProof.verify() function does the heavy lifting: it hashes pairs up the tree until it reaches the root, then checks if it matches.
Computing the Merkle Root (Off-Chain)
You don't compute the Merkle root in Solidity. That happens off-chain in JavaScript.
const { MerkleTree } = require('merkletreejs');
const keccak256 = require('keccak256');
const ethers = require('ethers');
const addresses = ['0x123...', '0x456...', '0x789...'];
const amounts = [100, 200, 150];
// Create leaves
const leaves = addresses.map((addr, i) =>
keccak256(ethers.utils.solidityPack(
['address', 'uint256'],
[addr, amounts[i]]
))
);
// Build tree
const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });
const root = tree.getRoot().toString('hex');
// Generate proof for first address
const proof = tree.getProof(leaves[0]);
console.log('0x' + root.toString('hex'));
console.log(proof.map(x => '0x' + x.data.toString('hex')));
That root gets deployed in the contract. Proofs get distributed to users (usually via API or frontend). Make sure your sorting is consistent between off-chain and on-chain or proofs will fail.
The Web2 Developer's Concern
I'll be honest: the first time I saw a Merkle tree in crypto, I thought, "Why not just use a database?" The answer is you can't trust the database anymore when you don't control the server. But Merkle trees are genuinely clever—they're deterministic, cryptographically secure, and scale logarithmically.
What Merkle Trees Can't Do
Merkle proofs only prove inclusion. They don't prove exclusion—if you want to prove someone is not in the airdrop list, that's harder (you need a range proof or accumulator, different tools). Also, once you post a Merkle root on-chain, changing it requires a new transaction. The Uniswap airdrop merkle root is still there, immutable, forever.
Next Step: Build It Today
Clone the merkletreejs repo and run the example with Hardhat. Create a simple airdrop for 10 test addresses, deploy it to Goerli testnet, and claim it yourself. You'll understand more in 30 minutes of hands-on work than reading another article.
Top comments (0)