Understanding BlockDAGs and GHOSTDAG
My WebsiteSupport My Work
  • Welcome!
  • Preface
  • Introduction
  • Part 1: BlockChains and BlockDAGs
    • Introduction
    • Chapter 1: From BFT to PoW
      • Byzantine Fault Tolerance
      • Proof-of-Work (PoW)
      • How PoW Works*
      • PoS Vs. PoW
      • Exercises
    • Chapter 2: the Block Chain Paradigm
      • A Graph Theory Primer
      • The Paradigm
      • Honesty and Rationality
      • Three Chain Selection Rules
      • Security Notions
      • A Security Model for Blockchain Consensus
      • Selfish Mining in Bitcoin
      • Safety
      • Liveness
      • Confirmation Times
      • Proving Bitcoin's Security*
      • Exercises
  • Part 2: The GHOSTDAG Protocol
    • Page 2
  • Supplementary Material
    • Math
      • Stuff You Should Know
        • Asymptotics, Growth, and Decay
        • Binomial Coefficients
        • The Binomial Formula
        • Geometric Sums and Series
      • Probability Theory
        • Probability Spaces
        • Random Variables
        • The Math of Block Creation
    • Computer Science
      • Stuff you Should Know
        • Asymptotic Notation
        • Negligible Functions
      • Cryptography
        • Random Oracles
        • Merkle Trees
        • One Time Pads
Powered by GitBook
On this page
  1. Supplementary Material
  2. Computer Science
  3. Cryptography

Merkle Trees

PreviousRandom OraclesNextOne Time Pads

Last updated 2 months ago

Merkle trees are a clever application of hashes and are ubiquitous throughout cryptocurrencies. They have been a cornerstone of the architecture since as early as the Bitcoin whitepaper.

Recall that a hash H\mathsf{H}H is some procedure that takes arbitrary data (for example, a block header), and outputs a binary string of some fixed length ℓ\ellℓ. The fact that the length is fixed is crucial. It means that no matter what you put into H\mathsf{H}H, you get the same number of bits back. Most hashes are 256256256 or 512512512 bits long.

The magic property we require from H\mathsf{H}H to be considered a hash function is collision-resistant. A collision is simply two distinct inputs x≠yx\ne yx=y such that H(x)=H(y)\mathsf{H}(x)=\mathsf{H}(y)H(x)=H(y). Defining collision-resistance precisely is as cumbersome as it is unnecessary. For our intents and purposes, it suffices to understand this notion as the proclamation that even if humanity diverted its entire resources towards finding a collision in H\mathsf{H}H, and kept at it for thousands of years, the probability of actually finding a collision remains smaller than throwing a hundred dice to find all of them balanced on one of their corners.

In other words, if we don't know xxx and yyy (or only know one of them), but we know that H(x)=H(y)\mathsf{H}(x) = \mathsf{H}(y)H(x)=H(y), we can safely infer that x=yx=yx=y.

Let us pave the way to Merkle trees by first working through a simpler problem.

Say I stored a large file FFF on a remote server and erased it from my computer. A year later, I needed the file, so I downloaded it. Let F′F'F′ be the file that I downloaded from the server, how can I be sure it is identical to FFF, and that no one tempered with my data?

I can do the following: before erasing FFF, I compute and store H(F)\mathsf{H}(F)H(F). That way, if I download a file F′F'F′ that is supposed to be a copy of FFF, I can verify its authenticity by computing H(F′)\mathsf{H}(F')H(F′) and verify that it is identical to the H(F)\mathsf{H}(F)H(F) I stored. I only know F′,H(F)F', \mathsf{H}(F)F′,H(F), and H(F′)\mathsf{H}(F')H(F′), but the fact that H(F)=H(F′)\mathsf{H}(F)=\mathsf{H}(F')H(F)=H(F′) together with collision-resistance, implies that F′=FF'=FF′=F.

How can we extend this idea to accommodate many files F1,…,FnF_1,\ldots,F_nF1​,…,Fn​.

One way would be to compute and store H(F1),…,H(Fn)\mathsf{H}(F_1),\ldots,\mathsf{H}(F_n)H(F1​),…,H(Fn​), but this would require us to store nnn hashes, taking ℓ⋅n\ell\cdot nℓ⋅n bits! In other words, the storage we require grows linearly.

Another thing we could do is to hash a single concatenation of all files H(F1∥…∥Fn)H(F_1\|\ldots\|F_n)H(F1​∥…∥Fn​). This solution indeed only requires ℓ\ellℓ bits of storage, but it has another problem: I must download all files to verify them. I would really like a way to download only one of the files and verify it.

Is there a way that on the one hand allows us to verify each file independently, and on the other does not require a lot of storage? Yes there is, a Merkle tree.

Merkle trees are simple to understand but a bit hard to explain. I find that the best way is to go through the details of a small example: four files. After explaining it in some detail, it suffices to describe the bold strokes of the general construction, leaving the details to the reader.

So Let us start with four files, F1,F2,F3,F4F_1,F_2,F_3,F_4F1​,F2​,F3​,F4​. Why four? We first compute the hashes H(F1),…,H(F4)\mathsf{H}(F_1),\ldots,\mathsf{H}(F_4)H(F1​),…,H(F4​), and place the result in the leaves of a binary tree:

To make things simple, we mark the value of each node with vwv_wvw​ where www is a binary string describing the path from the root to that node: reading left to right, a 000 denotes descending left whereas a 111 denotes descending right. For example, for the leftmost leaf we have H(F1)=v0,0H(F_1) = v_{0,0}H(F1​)=v0,0​, because starting from the root, we descend left twice to reach it. With this notation, we can define the value of each node to be the hash of its two children.

The value vvv is called the Merkle root, and it is the only value we store. That is, no matter how many files we backup, we will always need exactly ℓ\ellℓ bytes.

Now what if we want to download and verify the file F3F_3F3​? The server sends us the file, along with the values v1,1v_{1,1}v1,1​, and v0v_0v0​. That is, the values of all nodes that are exactly once step away from the path from the root to the hash of F3F_3F3​. The data (v1,1,v0)(v_{1,1},v_0)(v1,1​,v0​) is called the Merkle proof.

Why is the proof useful? Note that we know F3F_3F3​, so we can compute v1,0=H(F3)v_{1,0} = \mathsf{H}(F_3)v1,0​=H(F3​). We know v1,1v_{1,1}v1,1​ from the proof, so we can now compute v1=H(v1,0,v1,1)v_1 = \mathsf{H}(v_{1,0},v_{1,1})v1​=H(v1,0​,v1,1​). We know v0v_0v0​ from the proof, so we can finally compute v=H(v0,v1)v=\mathsf{H}(v_0,v_1)v=H(v0​,v1​) and compare it to the Merkle root I stored.

A quick inductive argument shows that if there is a fake Merkle proof for another file FFF that somehow passes this validation, then somewhere along the path we will find a collision in H\mathsf{H}H, contradiction collision-resistance.

Proof for four files

Let F3′F'_3F3′​ be the file we got. Let v1,1′v_{1,1}'v1,1′​ an v0′v_0'v0′​ be the fake Merkle proof. Let v1′v_1'v1′​ and v1,0′v_{1,0}'v1,0′​ be the values we compute through the verification process. That is v1,0′=H(F3′)v'_{1,0}=\mathsf{H}(F_3')v1,0′​=H(F3′​) and v1′=H(v1,0′,v1,1′)v'_1 = \mathsf{H}(v_{1,0}',v_{1,1}')v1′​=H(v1,0′​,v1,1′​).

If the verification passes, we get that v=H(v0,v1)=H(v0′,v1′)v = \mathsf{H}(v_0,v_1) =\mathsf{H}(v_0',v_1')v=H(v0​,v1​)=H(v0′​,v1′​), so from the collision resistance we get that v1=v1′v_1 = v_1'v1​=v1′​ (and v0=v0′v_0=v_0'v0​=v0′​, which is not important for the proof, but is important for other reasons). The second equality can be rewritten as H(v1,0,v1,1)=H(v1,0′,v1,1′)\mathsf{H}(v_{1,0},v_{1,1}) = \mathsf{H}(v'_{1,0},v'_{1,1}) H(v1,0​,v1,1​)=H(v1,0′​,v1,1′​) from which we learn that v1,0′=v1,0v'_{1,0} = v_{1,0}v1,0′​=v1,0​, or in other words H(F3′)=H(F3)\mathsf{H}(F_3') = \mathsf{H}(F_3)H(F3′​)=H(F3​). We use collision-resistance for a third time to learn that F3′=F3F_3' = F_3F3′​=F3​ as needed.

Note that the size of the Merkle proof is one less than the height of the tree. For four files, the size of the Merkle proof is 2⋅ℓ2\cdot \ell2⋅ℓ. For more files, the process looks like this.

For nnn files, the size of the only thing we need to store — the Merkle root — remains one hash. The size of the proof increases by one hash every time the number of files doubles. In other words, the Merkle proof contains log⁡n\log nlogn hashes. Given the file FFF, and the Merkle proof, I can easily compute all the hashes along the Merkle path, all the way up to the root. So if I have, say, a thousand files. The Merkle proof will be nine to ten hashes long.

At this point some of you might be asking: "wait, wasn't the entire motivation to verify files separately? If we just verify the entire block content, why not just hash all of the transactions together?". Ah, nice observation. The motivation for using a Merkle tree is exactly so that a user could easily verify that a transaction is in a given block by only downloading its header. The header contains a Merkle root of all transactions in the block. This is the idea of Simplified Payment Verification (SPV) nodes. They only store block headers and discard block data. Thanks to Merkle proofs, users can still verify their transactions are on the blockchain.

This cool trick is used in many ways in cryptography and cryptocurrencies. One example is the block header. Recall that when we , we said that the block header must contain information that ties it to the contents of the block, without making the header too large. Well, this is it. When creating the block, the miner computes a Merkle root of all transactions therein and places it in the block. When given a list of transactions that were allegedly the contents of this block, one could compute the Merkle tree for themselves to verify this.

described how proof-of-work works