Skip to content
Blockchain Association of Cameroon
BAC-L0-FOUND-1063 h

Merkle trees and inclusion proofs

How to prove a piece of data belongs to a set without transmitting the set. The structure that makes light clients and availability sampling possible.

  • Build a Merkle tree from a set of transactions
  • Produce and verify an inclusion proof
  • Compute proof size as a function of leaf count

A block can hold thousands of transactions. How do you prove to a cheap phone, on a limited connection, that your payment is in it, without sending the whole block? The answer is the Merkle tree: a single 32-byte digest summarises the entire set, and a short list of digests is enough to prove that a piece of data belongs to it. In this workshop you build a tree by hand, verify a proof, compute the size of a proof for a million leaves, and run a real attack against a badly designed tree.

Lessons 1/4 · 20 min

Building a Merkle tree

The starting question

A shopkeeper receives a batch of four payments. They want a single short value that changes if any of the four is altered, and that later lets them prove a specific payment belongs to the batch. Hashing the concatenation of the whole batch gives the first property but not the second: to prove one payment, you would have to hand over the other three. The Merkle tree gives both.

The concept (this workshop's toy convention)

Rules that every tool in the course applies:

  1. Leaf = SHA-256 of the data's text (for example a transaction).
  2. Parent node = SHA-256 of the two child digests concatenated as raw bytes, left then right.
  3. If a level has an odd number of nodes, the last one is duplicated.
  4. Repeat until a single node remains: the Merkle root.

A worked example: four transactions

Texts: tx0:Amina->Bello:20, tx1:Bello->Chantal:5, tx2:Chantal->Didier:12, tx3:Didier->Amina:7. Digests computed with SHA-256, truncated to 8 hex characters for readability:

NodeComputationStart of digest
L0SHA-256(tx0)801c17a0…
L1SHA-256(tx1)251f1575…
L2SHA-256(tx2)b25a5829…
L3SHA-256(tx3)f62a1b13…
N1SHA-256(L0 ‖ L1)82bcaf46…
N2SHA-256(L2 ‖ L3)024b1837…
RootSHA-256(N1 ‖ N2)803bff37…

The symbol ‖ means concatenation. You can recompute every row with the tools: sha256 for leaves, hexhash for nodes (entering the two full digests one after the other).

What the root guarantees

  • If one leaf changes by a single bit, its leaf digest changes, then every node on the path up to the root: the root changes (avalanche effect, workshop 104).
  • The root also depends on the order of the leaves: swapping tx0 and tx1 gives another root, because L0 ‖ L1 ≠ L1 ‖ L0.
  • This assumes SHA-256 is collision resistant: no one currently knows how to produce two different inputs with the same digest.

A case with an odd number of leaves

With three leaves a, b, c: compute N1 = H(La ‖ Lb), then N2 = H(Lc ‖ Lc) (c is duplicated), then the root H(N1 ‖ N2). With five leaves the duplication occurs at two levels. The merkle-explorer tool shows it: enter 5 leaves and watch the copied nodes.

What this does NOT guarantee

The root says nothing about the validity of the data (a transaction can be fraudulent and still sit prettily in the tree), nor about its origin: it is a digest, not a signature. And the duplication rule has a downside, seen in lesson 3.

Recap

  • Root = a 32-byte summary of the whole set, sensitive to every bit and to order.
  • Leaf = H(data); node = H(left ‖ right); odd level: duplicate the last node.
  • The root proves a set's consistency, not its validity.

Interactive tool

SHA-256 digest

Type some text: the digest updates on every keystroke. Compare with a second text to see the avalanche effect.

Digest (64 hex characters)

…

Digest of the second text

…

Differing characters are underlined and bold. Text is UTF-8 encoded with no space or newline added.

Interactive tool

SHA-256 of hex bytes

Paste bytes as hex: the hash covers the bytes, not the text of the digits. A second field is concatenated after the first.

Digest

…

Spaces, newlines and 0x prefixes are ignored.

Interactive tool

Merkle tree explorer (toy)

One leaf per line (2 to 8). Pick a leaf to see its inclusion proof; edit a text to see which nodes change.

5 leaves

Nodes changed since the last valid input are underlined and marked "changed". Nodes on the chosen leaf's path are outlined. Last node of an odd level: it is duplicated as its own sibling.

Convention: leaf = SHA-256(UTF-8 text); parent = SHA-256(left ‖ right, raw bytes); odd node duplicated. Toy tree: Bitcoin double-hashes and serialises differently.

Lessons 2/4 · 20 min

An inclusion proof: the path to the root

The starting question

Didier claims: "my payment tx2 is in this batch whose root is 803bff37…". You do not want to download the four transactions. What do you strictly need to be convinced?

The concept

An inclusion proof for a leaf lists, from the leaf up to the root, the sibling of each node on the path: the digest it is combined with at that level. The verifier receives the data and this list, and recomputes the root.

Side rule, used by the tool and by the exercises: at each level, if the current index is even, the sibling is on the right; if odd, on the left. Then the index is halved (rounded down).

A worked example: proving tx2 (index 2)

Tree from lesson 1. Leaf tx2 has index 2, which is even.

  1. Level 0: even index 2 → the sibling is on the right: L3 (f62a1b13…). Compute N2 = SHA-256(L2 ‖ L3). New index: 1.
  2. Level 1: odd index 1 → the sibling is on the left: N1 (82bcaf46…). Compute SHA-256(N1 ‖ N2). This is the candidate root.

The proof therefore holds only two digests: L3 and N1, i.e. 64 bytes, not the other three transactions. If the result equals the announced root (803bff37…), the proof is accepted.

Why the proof "binds" the leaf

To forge a proof for other data X', you would need to find siblings such that the recomputation yields the same root. If the tree's structure is fixed without ambiguity, that amounts to finding a collision or a preimage of SHA-256, which no one knows how to do. The proof is then binding for this root, at this position.

Your turn

In merkle-explorer, enter four leaves and click the third: the tool shows its proof and verifies it. Then change one character of the leaf in the proof (without touching the announced root): verification fails. Then change one of the siblings: it fails too.

What the proof does NOT guarantee

  • The root itself. If an attacker gives you a fake root with consistent fake proofs, everything looks right. The root must come from a trusted source. In Bitcoin it sits in the 80-byte block header, and a light client follows the header chain with the greatest cumulative work (see section 8 of the Bitcoin paper).
  • Transaction validity. The proof says "included", not "compliant with the rules": a light client relies on the fact that a block accepted by the network was validated.
  • Absence. No proof is not a proof of absence (see lesson 4 for sorted trees).

Recap

  • A proof = the siblings along the path; the verifier recomputes the root.
  • Even index → sibling on the right; odd → on the left; index ÷ 2 at each level.
  • The proof binds data to a root, but the root must already be trustworthy.

Interactive tool

Merkle tree explorer (toy)

One leaf per line (2 to 8). Pick a leaf to see its inclusion proof; edit a text to see which nodes change.

5 leaves

Nodes changed since the last valid input are underlined and marked "changed". Nodes on the chosen leaf's path are outlined. Last node of an odd level: it is duplicated as its own sibling.

Convention: leaf = SHA-256(UTF-8 text); parent = SHA-256(left ‖ right, raw bytes); odd node duplicated. Toy tree: Bitcoin double-hashes and serialises differently.

Interactive tool

SHA-256 of hex bytes

Paste bytes as hex: the hash covers the bytes, not the text of the digits. A second field is concatenated after the first.

Digest

…

Spaces, newlines and 0x prefixes are ignored.

Lessons 3/4 · 20 min

Proof size: the logarithm

The starting question

A block holds 1,000,000 transactions (a course assumption). How many bytes do you need to prove the inclusion of one transaction? A million? A thousand? Fewer?

The concept

At each level of the tree the number of nodes roughly halves. The path from a leaf to the root therefore crosses as many levels as the number of halvings needed to go from n to 1. For n leaves (n ≥ 2), the proof holds

⌈log₂ n⌉ digests

where ⌈ ⌉ means rounding up. Each SHA-256 digest is 32 bytes.

Worked examples

Leaves n⌈log₂ n⌉ (digests in the proof)Proof sizeAll leaf digests
4264 bytes128 bytes
8396 bytes256 bytes
1,00010 (since 2^10 = 1,024 ≥ 1,000)320 bytes32,000 bytes
1,000,00020 (since 2^20 = 1,048,576)640 bytes32,000,000 bytes (32 MB)

Doubling the number of leaves adds only one digest to the proof. The million leaves are proved with 640 bytes; for comparison, the full million digests weigh 32 MB. This property is what makes light clients possible on a phone with a limited data plan.

Check with 5 leaves: the levels have 5, 3, 2, then 1 nodes: three steps, and ⌈log₂ 5⌉ = 3. ✔ Duplication does not change the formula: it supplies a sibling (a copy) where one is missing.

Your turn

In the tool, enter 8 leaves: you will see proofs of 3 digests. With 5 leaves (levels of 5, 3, 2, then 1 nodes), proofs have 3 as well; for the last leaf, one of the siblings is a copy of itself. The tool stops at 8 leaves: for 9 leaves (levels of 9, 5, 3, 2, then 1 nodes), count by hand: every proof has 4 digests, which the independent lab will check in code.

A downside of the duplication rule

Take three leaves [a, b, c] and four leaves [a, b, c, c]. In the first case, c is duplicated to form N2 = H(Lc ‖ Lc); in the second, c and its copy form the same pair. The two lists give the same root (in the toy convention, d31a37ef… with text leaves "a", "b", "c"). Two different sets, one root: the root alone cannot tell "3 transactions" from "3 transactions whose last one is repeated". Bitcoin applies this same rule of duplicating the last hash, and the flaw gave rise there to a known vulnerability (block malleability, not a theft of funds) that implementations had to handle; for it to work, the duplicate must pass the other validation rules. Protocols that care detect such duplicates or change the convention.

What this does NOT guarantee

"Small" does not mean "free": the verifier must also know the position (or the sides) and the leaf's data. And proving one transaction says nothing about the others. Related ideas (Merkle trees over redundant data) are also used for data availability sampling in some chain designs; that topic is outside this workshop.

Recap

  • A proof has ⌈log₂ n⌉ digests of 32 bytes: 20 for a million leaves, i.e. 640 bytes.
  • Doubling the set adds one digest, not twice as many.
  • With last-node duplication, two different lists can share a root.

Interactive tool

Merkle tree explorer (toy)

One leaf per line (2 to 8). Pick a leaf to see its inclusion proof; edit a text to see which nodes change.

5 leaves

Nodes changed since the last valid input are underlined and marked "changed". Nodes on the chosen leaf's path are outlined. Last node of an odd level: it is duplicated as its own sibling.

Convention: leaf = SHA-256(UTF-8 text); parent = SHA-256(left ‖ right, raw bytes); odd node duplicated. Toy tree: Bitcoin double-hashes and serialises differently.

Lessons 4/4 · 25 min

When the tree lies: domain separation

The starting question

In our convention, a leaf is SHA-256(data) and a node is SHA-256(left ‖ right). Both are therefore "SHA-256 of something". Can a verifier confuse a leaf with an internal node? If so, what can an attacker do with that?

The attack, step by step

Take the four-transaction tree (lesson 1) and its root R = 803bff37…

An internal node N1 equals SHA-256(L0 ‖ L1). The bytes L0 ‖ L1 are 64 bytes long. An attacker says: "here is a 64-byte piece of data, equal to L0 ‖ L1, it is my leaf; here is my proof: a single sibling, N2, on the right". The verifier applies the rule:

  1. leaf = SHA-256(data) = SHA-256(L0 ‖ L1) = N1;
  2. candidate root = SHA-256(N1 ‖ N2) = R ✔.

The proof is accepted because the verifier does not check the proof's length (a verifier that fixed the expected depth, two levels for four leaves, would reject a one-level proof), and yet the "leaf" is not a transaction: it is an internal node in disguise, and the proof has one level instead of two. This is a second-preimage attack on the tree (more precisely, a confusion between levels): the attacker broke no hash function; they exploited an ambiguity of structure.

The defence: domain separation

Prefix the two kinds of hashing with a different byte:

  • leaf = SHA-256(0x00 ‖ data)
  • node = SHA-256(0x01 ‖ left ‖ right)

Thus a value computed as a leaf cannot be mistaken for a node, or the reverse, because the inputs differ from the very first byte. This is the solution used by the certificate transparency protocol (RFC 6962). Other defences are possible: fix the expected depth of the tree, or require leaves of a length other than 64 bytes. Bitcoin's transaction tree does not use these prefixes; the topic is known to researchers and goes beyond this workshop.

What the proof still does not guarantee

  • Even with domain separation, a proof establishes inclusion, not absence: to prove that data is not in a set, one can use a tree whose leaves are sorted: you then prove the inclusion of two consecutive neighbours a < x < b, between which x would have to sit. This is the subject of the independent lab.
  • A proof holds only for the given root. A root that changes (a new block) requires new proofs.

Your turn

Do the security exercise: you will run the attack with hexhash on real digests.

Recap

  • Without separation, a leaf and an internal node are indistinguishable: a node can pass as a leaf.
  • A distinct prefix (0x00 / 0x01) removes the ambiguity.
  • An inclusion proof never proves absence.

Interactive tool

SHA-256 of hex bytes

Paste bytes as hex: the hash covers the bytes, not the text of the digits. A second field is concatenated after the first.

Digest

…

Spaces, newlines and 0x prefixes are ignored.

Guided lab

In this lab you compute a Merkle root, verify a proof and estimate its size. The data is generated for you at each attempt: there is no point copying someone else's answer. The browser does not run your own Python code (that is the independent lab): you use the sha256 and hexhash tools, which do the same computation as the program you will write afterwards.

A browser is no substitute for a real terminal: this lab trains reading and reasoning; the real hands-on practice is in the independent lab.

A certified account is required

Graded activities record results in your name and are the basis for a credential. They therefore need a complete certified file and a current consent. Filling in the file does not verify your identity; the Secretariat can verify it afterwards ("Verified" badge).

  1. 1.Compute a five-leaf root

    Read the five leaf texts in the brief. With sha256, compute each one's digest. With hexhash, combine digests two by two (left then right) to get the next level; since there are five leaves, the fifth is duplicated. Continue to the root and enter it in hex.

    Interactive tool

    SHA-256 digest

    Type some text: the digest updates on every keystroke. Compare with a second text to see the avalanche effect.

    Digest (64 hex characters)

    …

    Digest of the second text

    …

    Differing characters are underlined and bold. Text is UTF-8 encoded with no space or newline added.

    Interactive tool

    SHA-256 of hex bytes

    Paste bytes as hex: the hash covers the bytes, not the text of the digits. A second field is concatenated after the first.

    Digest

    …

    Spaces, newlines and 0x prefixes are ignored.

  2. 2.Explore the tree and proofs

    In merkle-explorer, enter 8 leaves. Click the 6th leaf (index 5) and read its proof: how many digests, and on which side is each sibling? Then edit a leaf and watch the root. There is nothing to submit here; what you observe feeds the next two steps.

    Interactive tool

    Merkle tree explorer (toy)

    One leaf per line (2 to 8). Pick a leaf to see its inclusion proof; edit a text to see which nodes change.

    5 leaves

    Nodes changed since the last valid input are underlined and marked "changed". Nodes on the chosen leaf's path are outlined. Last node of an odd level: it is duplicated as its own sibling.

    Convention: leaf = SHA-256(UTF-8 text); parent = SHA-256(left ‖ right, raw bytes); odd node duplicated. Toy tree: Bitcoin double-hashes and serialises differently.

  3. 3.Verify an inclusion proof

    The brief gives a leaf, its index, its siblings in order and the announced root, for an 8-leaf tree. Apply the side rule (even index: sibling on the right, odd: on the left; index ÷ 2 at each level) with hexhash. Enter the recomputed root, then say whether the proof establishes inclusion. Careful: the proof may have been tampered with, either in the leaf or in a sibling.

    Interactive tool

    SHA-256 digest

    Type some text: the digest updates on every keystroke. Compare with a second text to see the avalanche effect.

    Digest (64 hex characters)

    …

    Digest of the second text

    …

    Differing characters are underlined and bold. Text is UTF-8 encoded with no space or newline added.

    Interactive tool

    SHA-256 of hex bytes

    Paste bytes as hex: the hash covers the bytes, not the text of the digits. A second field is concatenated after the first.

    Digest

    …

    Spaces, newlines and 0x prefixes are ignored.

  4. 4.Estimate a proof's size

    A fictitious block holds 3,000 transactions. Compute how many 32-byte SHA-256 digests an inclusion proof for one of them contains in the lesson-3 tree, then answer the check question. Hint: find the smallest power of 2 that reaches or exceeds 3,000.

Security exercise — Run the attack: an internal node disguised as a leaf

A toy verifier applies the workshop's convention without domain separation: leaf = SHA-256(data bytes), node = SHA-256(left ‖ right). It knows this published root for a batch of four (fictitious) transactions:

root R = 803bff37ed78fb52ccf54851b3ca635e05a4c004fa6dbbe4863ff21c4e8f589c

An attacker presents:

  • a 64-byte "piece of data": L0 ‖ L1, i.e. the concatenation of the following two leaf digests;
  • a single-level proof: the sibling N2, placed on the right.
L0 = 801c17a0d398ef843a0a781c773fe759a8a8542e4850e58224a344140642da2e
L1 = 251f15750c47f9694536abb76897fec69513cea2e42cf05d872874604c5bfcac
N2 = 024b1837a1edc8fe1fbb1ed744fa946674e22d46791ad22228678e8183a80bf8

Procedure with the hexhash tool:

  1. Enter L0 then L1 (two items concatenated): note the digest. This will be the "leaf" as the verifier sees it.
  2. Enter that digest then N2: note the resulting digest.

Enter the first 8 hex characters of the digest obtained at step 2. Compare them with the start of R. Then ask yourself: which byte prefix would you add to the leaf and to the node so that this proof is rejected?

Workshop quiz

Eight questions drawn from a larger pool, 70% to pass. It opens once every lab checkpoint is passed. You can retake it: the draw changes.

A certified account is required

Graded activities record results in your name and are the basis for a credential. They therefore need a complete certified file and a current consent. Filling in the file does not verify your identity; the Secretariat can verify it afterwards ("Verified" badge).

Independent lab (outside the site)

Not graded online: do it on your own machine and publish it to your Git repository.

To do on your machine, outside the site, in a Git repository, in Python (standard library, hashlib module).

  1. Write merkle.py with three functions: build(leaves) returning all the tree's levels (workshop convention: leaf = SHA-256(text), node = SHA-256(left ‖ right), last node of an odd level duplicated), prove(levels, index) returning the list of (sibling, side), and verify(leaf, proof, root).
  2. Test them with lesson 1's four transactions: the root must start with 803bff37. Then with 1 to 20 leaves: verify the proof of every leaf.
  3. Measure the growth: for n = 2, 4, 8, ..., 2^16, print the proof length and compare it to ⌈log₂ n⌉.
  4. Reproduce lesson 4's attack in code (a 64-byte piece of data presented as a leaf). Then add the 0x00 and 0x01 prefixes and show the attack fails.
  5. To go further: sort the leaves, and write a non-inclusion proof for an absent value by providing the inclusion proofs of its two consecutive neighbours (handling the edges: before the first leaf and after the last) and checking that their indices are consecutive, i and i + 1. Note in a NOTES.md file what this proof assumes (a sorted tree and a trusted root).

Deliverable: merkle.py, your tests, NOTES.md. The data stays fictitious.

Takeaways

  • The Merkle root summarises a whole set in 32 bytes; changing one bit of a leaf, or the order, changes the root.
  • An inclusion proof lists the siblings along the leaf-to-root path; the verifier recomputes the root and compares it with the one already known.
  • Proof size grows as ⌈log₂ n⌉: 20 digests (640 bytes) for a million leaves.
  • A proof binds data to a root, but guarantees neither the root's authenticity, nor the data's validity, nor the absence of other data.
  • Without domain separation (0x00 / 0x01 prefixes), an internal node can pass as a leaf.

Common misconceptions

  • Misconception : "A valid Merkle proof proves the transaction is valid."

    In fact : It only proves the data sits at this position under this root. Validity (balance, signature) is checked elsewhere.

  • Misconception : "The proof for a million leaves holds a million digests, or at least a thousand."

    In fact : It holds ⌈log₂ n⌉ of them: 20 for a million, i.e. 640 bytes.

  • Misconception : "If I receive no proof for a piece of data, it is not in the tree."

    In fact : No proof proves nothing. Proving absence needs a special structure, such as a sorted tree with two neighbours.

  • Misconception : "A valid proof is enough, even if the root comes from a stranger."

    In fact : An attacker can craft a consistent root and proofs. The root must come from a trusted source, for example a block header the client follows.

Glossary

Merkle root
The digest at the top of the tree; it summarises all the leaves, in order, in 32 bytes for SHA-256.
Leaf
A bottom node of the tree: the digest of one piece of data (for example a transaction).
Inclusion proof
The list of sibling digests along the path from the leaf to the root, letting a verifier recompute the root.
Domain separation
A technique that prefixes leaf hashes and node hashes differently (for example 0x00 and 0x01) so that a value of one kind is never taken for the other.
Light client
A program that downloads only block headers and verifies inclusion proofs, instead of all blocks.