P04: Build a Blockchain from Scratch

P04: Build a Blockchain from Scratch

Project Overview

Attribute Value
Main Language Go
Alternative Languages Rust, Python, TypeScript
Difficulty Advanced
Coolness Level Level 4: Hardcore Tech Flex
Business Potential Portfolio Showcase / Resume Gold
Knowledge Area Distributed Systems / Blockchain Core
Main Book โ€œMastering Bitcoinโ€ by Andreas M. Antonopoulos

Learning Objectives

By completing this project, you will:

  1. Understand the blockchain data structure at a fundamental level, including block headers, transaction bodies, hash chaining, and why immutability emerges from these design choices
  2. Implement the UTXO transaction model used by Bitcoin, grasping how unspent transaction outputs create a verifiable ownership graph without requiring account balances
  3. Master chain selection algorithms including the longest-chain rule and how forks are detected, validated, and resolved in a decentralized system
  4. Build a peer-to-peer gossip network that propagates blocks and transactions across nodes, handling network partitions, conflicting data, and Byzantine peers
  5. Apply cryptographic primitives you learned in previous projects (hashing, digital signatures) in a real distributed system context

Deep Theoretical Foundation

What Is a Blockchain, Really?

Forget the buzzwords. A blockchain is simply an append-only linked list where each node (block) contains a cryptographic hash of the previous node. This creates a property called tamper evidence: if anyone modifies any block, every subsequent blockโ€™s hash becomes invalid.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                        Blockchain as a Data Structure                        โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚ Block 0     โ”‚     โ”‚ Block 1     โ”‚     โ”‚ Block 2     โ”‚     โ”‚ Block 3   โ”‚  โ”‚
โ”‚  โ”‚ (Genesis)   โ”‚     โ”‚             โ”‚     โ”‚             โ”‚     โ”‚           โ”‚  โ”‚
โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค     โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค     โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค     โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค  โ”‚
โ”‚  โ”‚ prev: NULL  โ”‚โ—€โ”€โ”€โ”€โ”€โ”‚ prev: H(0)  โ”‚โ—€โ”€โ”€โ”€โ”€โ”‚ prev: H(1)  โ”‚โ—€โ”€โ”€โ”€โ”€โ”‚prev: H(2) โ”‚  โ”‚
โ”‚  โ”‚ txs: [...]  โ”‚     โ”‚ txs: [...]  โ”‚     โ”‚ txs: [...]  โ”‚     โ”‚txs: [...] โ”‚  โ”‚
โ”‚  โ”‚ nonce: ...  โ”‚     โ”‚ nonce: ...  โ”‚     โ”‚ nonce: ...  โ”‚     โ”‚nonce: ... โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚         โ”‚                  โ”‚                   โ”‚                    โ”‚        โ”‚
โ”‚         โ–ผ                  โ–ผ                   โ–ผ                    โ–ผ        โ”‚
โ”‚      H(Block0)         H(Block1)           H(Block2)           H(Block3)    โ”‚
โ”‚      = 0x7a3f...       = 0x2e8d...         = 0x9c1a...         = 0x4f5b...  โ”‚
โ”‚                                                                              โ”‚
โ”‚  Tamper with Block 1's transactions?                                         โ”‚
โ”‚  โ†’ H(Block1) changes โ†’ Block 2's "prev" field is now wrong                  โ”‚
โ”‚  โ†’ H(Block2) changes โ†’ Block 3's "prev" field is now wrong                  โ”‚
โ”‚  โ†’ The entire chain after the modification becomes invalid                   โ”‚
โ”‚                                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

But hereโ€™s the key insight: tamper evidence is not tamper resistance. Anyone can create a valid alternative chain. The genius of Bitcoin was combining the blockchain data structure with:

  1. Proof-of-Work: Makes creating blocks expensive
  2. Longest-chain rule: Defines which chain is โ€œrealโ€
  3. P2P gossip: Propagates the consensus view

This project focuses on the data structure, UTXO model, and P2P networking. Project 5 adds proof-of-work mining.

The Block Structure

A block consists of two parts:

Block Header (fixed size, ~80 bytes in Bitcoin):

  • version: Protocol version
  • prev_hash: SHA-256 hash of the previous blockโ€™s header
  • merkle_root: Root of the Merkle tree of transactions
  • timestamp: Unix timestamp when the block was created
  • difficulty: The proof-of-work difficulty target
  • nonce: The value miners vary to find a valid hash

Block Body (variable size):

  • transactions: List of transactions included in this block
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                          Block Structure                            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                     โ”‚
โ”‚  HEADER (80 bytes)                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚  โ”‚ version      โ”‚ 4 bytes  โ”‚ Protocol rules this block follows  โ”‚ โ”‚
โ”‚  โ”‚ prev_hash    โ”‚ 32 bytes โ”‚ SHA256(SHA256(previous header))     โ”‚ โ”‚
โ”‚  โ”‚ merkle_root  โ”‚ 32 bytes โ”‚ Root of transaction Merkle tree     โ”‚ โ”‚
โ”‚  โ”‚ timestamp    โ”‚ 4 bytes  โ”‚ Block creation time (Unix epoch)    โ”‚ โ”‚
โ”‚  โ”‚ difficulty   โ”‚ 4 bytes  โ”‚ Proof-of-work target (compact form) โ”‚ โ”‚
โ”‚  โ”‚ nonce        โ”‚ 4 bytes  โ”‚ Value varied during mining          โ”‚ โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚                                                                     โ”‚
โ”‚  BODY (variable)                                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚  โ”‚ tx_count     โ”‚ varint   โ”‚ Number of transactions              โ”‚ โ”‚
โ”‚  โ”‚ transactions โ”‚ variable โ”‚ List of Transaction structures      โ”‚ โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚                                                                     โ”‚
โ”‚  Block Hash = SHA256(SHA256(header))                                โ”‚
โ”‚  Note: The hash is of the HEADER only, not the full block          โ”‚
โ”‚                                                                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Why hash only the header?

The header includes the Merkle root, which cryptographically commits to all transactions. This means:

  • Light clients can verify block headers without downloading full blocks
  • The block hash remains 32 bytes regardless of block size
  • Transaction verification can be done via Merkle proofs

The UTXO Transaction Model

Bitcoin doesnโ€™t track account balances. Instead, it tracks Unspent Transaction Outputs (UTXOs). Think of UTXOs as digital coins of specific denominations.

Transaction Structure:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                         Transaction Model                            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  Transaction                                                         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚
โ”‚  โ”‚ INPUTS (what you're spending)                                   โ”‚โ”‚
โ”‚  โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚โ”‚
โ”‚  โ”‚ โ”‚ prev_tx_hash   โ”‚ 32 bytes โ”‚ Hash of the transaction you're  โ”‚ โ”‚โ”‚
โ”‚  โ”‚ โ”‚                โ”‚          โ”‚ spending from                    โ”‚ โ”‚โ”‚
โ”‚  โ”‚ โ”‚ output_index   โ”‚ 4 bytes  โ”‚ Which output of that transaction โ”‚ โ”‚โ”‚
โ”‚  โ”‚ โ”‚ signature      โ”‚ variable โ”‚ Proof you can spend this UTXO    โ”‚ โ”‚โ”‚
โ”‚  โ”‚ โ”‚ public_key     โ”‚ 33 bytes โ”‚ Your public key (compressed)     โ”‚ โ”‚โ”‚
โ”‚  โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚โ”‚
โ”‚  โ”‚ (May have multiple inputs to combine UTXOs)                     โ”‚โ”‚
โ”‚  โ”‚                                                                  โ”‚โ”‚
โ”‚  โ”‚ OUTPUTS (what you're creating)                                  โ”‚โ”‚
โ”‚  โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚โ”‚
โ”‚  โ”‚ โ”‚ amount         โ”‚ 8 bytes  โ”‚ Value in satoshis (1e-8 BTC)    โ”‚ โ”‚โ”‚
โ”‚  โ”‚ โ”‚ pubkey_hash    โ”‚ 20 bytes โ”‚ Hash of recipient's public key  โ”‚ โ”‚โ”‚
โ”‚  โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚โ”‚
โ”‚  โ”‚ (May have multiple outputs for change, etc.)                    โ”‚โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚
โ”‚                                                                      โ”‚
โ”‚  Validation Rules:                                                   โ”‚
โ”‚  1. sum(inputs) >= sum(outputs)  (difference is miner fee)          โ”‚
โ”‚  2. Each input references a valid, unspent output                   โ”‚
โ”‚  3. Each input's signature is valid for the referenced output       โ”‚
โ”‚  4. No input is spent twice in the same transaction                 โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

UTXO Example:

Imagine Alice has 10 coins from a previous transaction. She wants to send 7 to Bob:

Before:
  UTXO Set: { (TxA, 0): 10 coins โ†’ Alice's pubkey_hash }

Alice creates Transaction B:
  Input:  (TxA, 0) + Alice's signature
  Output 0: 7 coins โ†’ Bob's pubkey_hash
  Output 1: 2.99 coins โ†’ Alice's pubkey_hash (change)
  (0.01 coins implicit fee to miner)

After:
  UTXO Set: {
    (TxB, 0): 7 coins โ†’ Bob's pubkey_hash,
    (TxB, 1): 2.99 coins โ†’ Alice's pubkey_hash
  }

The original UTXO (TxA, 0) is now SPENT and removed from the set.

Why UTXOs instead of Account Balances?

  1. Parallelization: Different UTXOs can be validated independently
  2. Privacy: Each transaction creates new addresses (though not perfect privacy)
  3. Simple Validation: Check that inputs exist and are unspent; no balance computation
  4. Atomic Operations: Either all inputs are spent or none are

Chain Selection: The Longest Chain Rule

When nodes disagree about which blocks are valid, they follow the longest chain rule (technically, the chain with the most accumulated proof-of-work):

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                         Fork Resolution                              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  Initial state (all nodes agree):                                    โ”‚
โ”‚                                                                      โ”‚
โ”‚  [Genesis] โ† [Block 1] โ† [Block 2] โ† [Block 3]                      โ”‚
โ”‚                                                                      โ”‚
โ”‚  Two miners find Block 4 simultaneously:                             โ”‚
โ”‚                                                                      โ”‚
โ”‚  [Genesis] โ† [Block 1] โ† [Block 2] โ† [Block 3] โ† [Block 4a]        โ”‚
โ”‚                                               โ””โ”€ [Block 4b]         โ”‚
โ”‚                                                                      โ”‚
โ”‚  Network splits: some nodes see 4a first, others see 4b             โ”‚
โ”‚  Both chains are valid! Nodes work on whichever they saw first.     โ”‚
โ”‚                                                                      โ”‚
โ”‚  Eventually, one chain gets another block:                           โ”‚
โ”‚                                                                      โ”‚
โ”‚  [Genesis] โ† [Block 1] โ† [Block 2] โ† [Block 3] โ† [Block 4a] โ† [5]  โ”‚
โ”‚                                               โ””โ”€ [Block 4b] (orphan)โ”‚
โ”‚                                                                      โ”‚
โ”‚  Now all nodes switch to the longer chain.                           โ”‚
โ”‚  Block 4b becomes an "orphan" or "stale" block.                     โ”‚
โ”‚  Transactions in 4b but not in 4a return to the mempool.            โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Key Properties of Longest-Chain:

  1. Consistency: Nodes eventually agree (probabilistic finality)
  2. Liveness: Valid transactions eventually get included
  3. Safety: The deeper a block, the harder to reverse (exponentially)

Chain Reorganization

When a node discovers a longer chain, it must:

  1. Identify the fork point: The common ancestor of current and new chain
  2. Rollback: Undo all blocks from tip to fork point (return UTXOs, etc.)
  3. Replay: Apply all blocks from fork point to new tip
  4. Update mempool: Transactions from orphaned blocks may need re-inclusion
func (bc *Blockchain) Reorganize(newChain []*Block) error {
    // Find fork point
    forkPoint := bc.FindCommonAncestor(newChain[0])

    // Collect transactions from orphaned blocks
    orphanedTxs := bc.CollectTransactions(forkPoint, bc.Tip)

    // Rollback UTXO set to fork point
    bc.UTXOSet.RollbackTo(forkPoint)

    // Apply new chain
    for _, block := range newChain {
        if err := bc.ApplyBlock(block); err != nil {
            return err
        }
    }

    // Return orphaned transactions to mempool (if still valid)
    bc.Mempool.ReinsertValid(orphanedTxs)

    return nil
}

P2P Network Architecture

Blockchain nodes communicate via a peer-to-peer gossip protocol:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                      P2P Network Topology                            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  Gossip Protocol:                                                    โ”‚
โ”‚                                                                      โ”‚
โ”‚      โ”Œโ”€โ”€โ”€โ”€โ”€โ”                 โ”Œโ”€โ”€โ”€โ”€โ”€โ”                                โ”‚
โ”‚      โ”‚Node1โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚Node2โ”‚                                โ”‚
โ”‚      โ””โ”€โ”€โ”ฌโ”€โ”€โ”˜                 โ””โ”€โ”€โ”ฌโ”€โ”€โ”˜                                โ”‚
โ”‚         โ”‚                       โ”‚                                    โ”‚
โ”‚         โ”‚      โ”Œโ”€โ”€โ”€โ”€โ”€โ”          โ”‚                                    โ”‚
โ”‚         โ””โ”€โ”€โ”€โ”€โ”€โ–บโ”‚Node3โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚                โ””โ”€โ”€โ”ฌโ”€โ”€โ”˜                                               โ”‚
โ”‚                   โ”‚                                                  โ”‚
โ”‚         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚         โ–ผ         โ–ผ         โ–ผ                                        โ”‚
โ”‚      โ”Œโ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚      โ”‚Node4โ”‚   โ”‚Node5โ”‚   โ”‚Node6โ”‚                                    โ”‚
โ”‚      โ””โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚                                                                      โ”‚
โ”‚  Message Types:                                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚ VERSION     โ”‚ Exchange protocol version and chain height     โ”‚   โ”‚
โ”‚  โ”‚ VERACK      โ”‚ Acknowledge version message                    โ”‚   โ”‚
โ”‚  โ”‚ INV         โ”‚ Inventory: "I have these blocks/transactions"  โ”‚   โ”‚
โ”‚  โ”‚ GETDATA     โ”‚ Request specific blocks/transactions           โ”‚   โ”‚
โ”‚  โ”‚ BLOCK       โ”‚ Full block data                                โ”‚   โ”‚
โ”‚  โ”‚ TX          โ”‚ Transaction data                               โ”‚   โ”‚
โ”‚  โ”‚ GETBLOCKS   โ”‚ Request block hashes from a starting point     โ”‚   โ”‚
โ”‚  โ”‚ HEADERS     โ”‚ Block headers (for SPV/light clients)          โ”‚   โ”‚
โ”‚  โ”‚ PING/PONG   โ”‚ Keep connection alive                          โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Gossip Process for New Block:

  1. Miner finds valid block
  2. Miner sends INV message to peers with block hash
  3. Peers that donโ€™t have this block send GETDATA
  4. Miner sends BLOCK with full block data
  5. Each receiving peer validates, adds to chain, and repeats step 2

Gossip Process for New Transaction:

  1. User broadcasts transaction to connected nodes
  2. Nodes validate transaction against current UTXO set
  3. If valid, add to mempool and send INV to peers
  4. Propagation continues until all nodes have the transaction

Mempool: The Waiting Room

The mempool (memory pool) holds valid, unconfirmed transactions:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                         Mempool Architecture                         โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                      โ”‚
โ”‚  Incoming Transaction โ†’ Validation โ†’ Mempool โ†’ Block Inclusion       โ”‚
โ”‚                                                                      โ”‚
โ”‚  Validation Checks:                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚ 1. Syntactically valid (correct format, signatures, etc.)   โ”‚    โ”‚
โ”‚  โ”‚ 2. All inputs reference existing, unspent outputs           โ”‚    โ”‚
โ”‚  โ”‚ 3. sum(inputs) >= sum(outputs)                              โ”‚    โ”‚
โ”‚  โ”‚ 4. Not already in mempool or blockchain                     โ”‚    โ”‚
โ”‚  โ”‚ 5. Not double-spending another mempool transaction          โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                                      โ”‚
โ”‚  Mempool Data Structure:                                             โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚ txid โ†’ Transaction                                          โ”‚    โ”‚
โ”‚  โ”‚ ancestor_set: dependencies on other mempool txs             โ”‚    โ”‚
โ”‚  โ”‚ fee_rate: satoshis per byte (for prioritization)            โ”‚    โ”‚
โ”‚  โ”‚ time_added: for expiration policies                         โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                                      โ”‚
โ”‚  Eviction Policy (when mempool is full):                             โ”‚
โ”‚  - Remove lowest fee-rate transactions first                        โ”‚
โ”‚  - Never evict transaction if descendant has higher fee-rate        โ”‚
โ”‚                                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Security Considerations

Double-Spend Attack: An attacker sends conflicting transactions to different parts of the network. The first to be included in a block wins; the other becomes invalid.

Mitigation: Wait for confirmations. With 6 blocks of confirmation, reversing a transaction requires controlling >50% of network hash power.

Eclipse Attack: An attacker surrounds a node with malicious peers, controlling all its network connections. The victim sees a fake view of the blockchain.

Mitigation: Connect to diverse peers, use hardcoded checkpoint blocks, verify against multiple sources.

Sybil Attack: Creating many fake identities to gain disproportionate influence.

Mitigation: Proof-of-Work ties influence to computational resources, not identity count.


Complete Project Specification

Functional Requirements

  1. Block Management
    • Create genesis block with hardcoded parameters
    • Validate block structure and hash linkage
    • Compute Merkle root from transactions
    • Store and retrieve blocks by hash
    • Track chain tip and total height
  2. Transaction Handling
    • Create transactions with inputs and outputs
    • Validate transaction signatures using ECDSA
    • Verify transactions against UTXO set
    • Calculate transaction fees
    • Serialize/deserialize transactions
  3. UTXO Set Management
    • Track unspent transaction outputs
    • Apply transactions (remove spent, add new UTXOs)
    • Rollback transactions during chain reorganization
    • Query balance by public key hash
  4. Mempool Operations
    • Accept and validate incoming transactions
    • Detect and reject double-spends
    • Prioritize transactions by fee rate
    • Remove transactions when included in blocks
  5. Chain Operations
    • Validate and add new blocks
    • Detect and resolve forks
    • Perform chain reorganization
    • Identify longest valid chain
  6. P2P Networking
    • Discover and connect to peers
    • Exchange version/handshake messages
    • Propagate transactions via gossip
    • Propagate blocks via gossip
    • Request missing blocks for sync
    • Handle peer disconnections gracefully

Command-Line Interface

# Initialize new blockchain with genesis block
$ blockchain init
Genesis block created: 0x000000000019d668...
Blockchain initialized at ./data/

# Start node and connect to network
$ blockchain node --port 8333 --peers "192.168.1.10:8333,192.168.1.11:8333"
Node started on port 8333
Connected to 2 peers
Chain height: 0
Syncing blocks...

# Create a new wallet (key pair)
$ blockchain wallet new
Private Key: 5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ
Public Key:  04d0de0aaeaefad02b8bdc8a01a1b8b11c696bd3d66a2c5f10780d95b7df42645cd...
Address:     1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2

# Check balance
$ blockchain balance 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2
Balance: 50.00000000 BTC (1 UTXO)

# Send transaction
$ blockchain send --from 5HueCGU8rMjxEX... --to 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa --amount 10.5
Transaction created: 0x3a7bd3e2360a3d...
Broadcasting to 5 peers...
Transaction accepted to mempool

# Mine a block (simplified, no real PoW)
$ blockchain mine --address 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2
Mining block with 15 transactions...
Block found: 0x00000000839a8e6886ab5951d76f4114...
Block height: 101
Reward: 50 BTC + 0.0015 BTC fees

# View block details
$ blockchain block 0x00000000839a8e6886ab5951d76f4114...
Block 0x00000000839a8e6886ab5951d76f4114...
  Height:      101
  Prev Hash:   0x000000004ebadb55ee9096c9a2f8880e...
  Merkle Root: 0x4a5e1e4baab89f3a32518a88c31bc87f...
  Timestamp:   2024-01-15 14:32:11 UTC
  Difficulty:  1d00ffff
  Nonce:       2083236893
  Transactions: 15

# View transaction
$ blockchain tx 0x3a7bd3e2360a3d...
Transaction 0x3a7bd3e2360a3d...
  Inputs:
    [0] TxID: 0x7b3f... Index: 0 (25 BTC)
  Outputs:
    [0] 10.5 BTC โ†’ 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
    [1] 14.4999 BTC โ†’ 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2 (change)
  Fee: 0.0001 BTC
  Status: Confirmed (12 blocks)

# View network status
$ blockchain status
Chain Height:  101
Connected Peers: 5
Mempool Size:  234 transactions
UTXO Set Size: 15,234 outputs

Solution Architecture

Module Structure

blockchain/
โ”œโ”€โ”€ cmd/
โ”‚   โ””โ”€โ”€ blockchain/
โ”‚       โ””โ”€โ”€ main.go              # CLI entry point
โ”œโ”€โ”€ internal/
โ”‚   โ”œโ”€โ”€ core/
โ”‚   โ”‚   โ”œโ”€โ”€ block.go             # Block type and methods
โ”‚   โ”‚   โ”œโ”€โ”€ block_test.go
โ”‚   โ”‚   โ”œโ”€โ”€ transaction.go       # Transaction type and methods
โ”‚   โ”‚   โ”œโ”€โ”€ transaction_test.go
โ”‚   โ”‚   โ”œโ”€โ”€ merkle.go            # Merkle tree construction
โ”‚   โ”‚   โ””โ”€โ”€ merkle_test.go
โ”‚   โ”œโ”€โ”€ chain/
โ”‚   โ”‚   โ”œโ”€โ”€ blockchain.go        # Blockchain state machine
โ”‚   โ”‚   โ”œโ”€โ”€ blockchain_test.go
โ”‚   โ”‚   โ”œโ”€โ”€ utxo_set.go          # UTXO management
โ”‚   โ”‚   โ”œโ”€โ”€ utxo_set_test.go
โ”‚   โ”‚   โ”œโ”€โ”€ mempool.go           # Transaction mempool
โ”‚   โ”‚   โ””โ”€โ”€ mempool_test.go
โ”‚   โ”œโ”€โ”€ crypto/
โ”‚   โ”‚   โ”œโ”€โ”€ hash.go              # SHA-256 utilities
โ”‚   โ”‚   โ”œโ”€โ”€ keys.go              # ECDSA key management
โ”‚   โ”‚   โ”œโ”€โ”€ signature.go         # Sign and verify
โ”‚   โ”‚   โ””โ”€โ”€ address.go           # Address encoding (Base58Check)
โ”‚   โ”œโ”€โ”€ network/
โ”‚   โ”‚   โ”œโ”€โ”€ peer.go              # Peer connection management
โ”‚   โ”‚   โ”œโ”€โ”€ peer_test.go
โ”‚   โ”‚   โ”œโ”€โ”€ protocol.go          # Message types and encoding
โ”‚   โ”‚   โ”œโ”€โ”€ server.go            # TCP server
โ”‚   โ”‚   โ”œโ”€โ”€ gossip.go            # Block/tx propagation
โ”‚   โ”‚   โ””โ”€โ”€ sync.go              # Initial block download
โ”‚   โ””โ”€โ”€ storage/
โ”‚       โ”œโ”€โ”€ store.go             # Abstract storage interface
โ”‚       โ”œโ”€โ”€ leveldb.go           # LevelDB implementation
โ”‚       โ””โ”€โ”€ memory.go            # In-memory for testing
โ”œโ”€โ”€ pkg/
โ”‚   โ””โ”€โ”€ types/
โ”‚       โ”œโ”€โ”€ types.go             # Common types (Hash, Address, etc.)
โ”‚       โ””โ”€โ”€ encoding.go          # Serialization helpers
โ”œโ”€โ”€ go.mod
โ”œโ”€โ”€ go.sum
โ””โ”€โ”€ README.md

Core Data Structures

// Hash represents a 32-byte SHA-256 hash
type Hash [32]byte

// Address is a Base58Check-encoded public key hash
type Address string

// Block represents a block in the blockchain
type Block struct {
    Header       BlockHeader
    Transactions []*Transaction
}

type BlockHeader struct {
    Version    uint32
    PrevHash   Hash
    MerkleRoot Hash
    Timestamp  int64
    Difficulty uint32
    Nonce      uint32
}

// Hash computes the double-SHA256 of the block header
func (bh *BlockHeader) Hash() Hash {
    data := bh.Serialize()
    first := sha256.Sum256(data)
    return sha256.Sum256(first[:])
}

// Transaction represents a transfer of value
type Transaction struct {
    Version  uint32
    Inputs   []TxInput
    Outputs  []TxOutput
    LockTime uint32
}

type TxInput struct {
    PrevTxHash  Hash
    OutputIndex uint32
    Signature   []byte
    PublicKey   []byte
}

type TxOutput struct {
    Amount       uint64  // In satoshis (1e-8 coins)
    PubKeyHash   [20]byte
}

// TxID computes the transaction hash
func (tx *Transaction) TxID() Hash {
    data := tx.Serialize()
    first := sha256.Sum256(data)
    return sha256.Sum256(first[:])
}

// UTXO represents an unspent transaction output
type UTXO struct {
    TxID        Hash
    OutputIndex uint32
    Output      TxOutput
    BlockHeight uint64
}

// Mempool holds unconfirmed transactions
type Mempool struct {
    transactions map[Hash]*Transaction
    byAddress    map[Address][]*Transaction  // Index for conflict detection
    mu           sync.RWMutex
}

// Blockchain manages the chain state
type Blockchain struct {
    store       Storage
    utxoSet     *UTXOSet
    mempool     *Mempool
    tip         Hash        // Current chain tip
    height      uint64
    mu          sync.RWMutex
}

Key Algorithms

Transaction Signature Verification

func (tx *Transaction) Verify(utxoSet *UTXOSet) error {
    for i, input := range tx.Inputs {
        // 1. Find the referenced UTXO
        utxo, exists := utxoSet.Get(input.PrevTxHash, input.OutputIndex)
        if !exists {
            return fmt.Errorf("input %d: UTXO not found", i)
        }

        // 2. Verify public key hashes to the UTXO's pubkey hash
        pubKeyHash := Hash160(input.PublicKey)
        if !bytes.Equal(pubKeyHash, utxo.Output.PubKeyHash[:]) {
            return fmt.Errorf("input %d: public key mismatch", i)
        }

        // 3. Verify signature
        sigHash := tx.SignatureHash(i)  // Hash of tx with input script cleared
        if !ecdsa.Verify(input.PublicKey, sigHash, input.Signature) {
            return fmt.Errorf("input %d: invalid signature", i)
        }
    }

    // 4. Verify sum(inputs) >= sum(outputs)
    inputSum := tx.InputSum(utxoSet)
    outputSum := tx.OutputSum()
    if inputSum < outputSum {
        return fmt.Errorf("output exceeds input: %d < %d", inputSum, outputSum)
    }

    return nil
}

Block Validation

func (bc *Blockchain) ValidateBlock(block *Block) error {
    // 1. Verify block header hash meets difficulty target
    blockHash := block.Header.Hash()
    if !bc.MeetsDifficulty(blockHash, block.Header.Difficulty) {
        return errors.New("block hash does not meet difficulty")
    }

    // 2. Verify prev_hash points to a known block
    if !bc.HasBlock(block.Header.PrevHash) {
        return errors.New("previous block not found")
    }

    // 3. Verify timestamp is reasonable
    prevBlock := bc.GetBlock(block.Header.PrevHash)
    if block.Header.Timestamp <= prevBlock.Header.Timestamp {
        return errors.New("timestamp not greater than previous")
    }

    // 4. Verify Merkle root
    computedRoot := ComputeMerkleRoot(block.Transactions)
    if computedRoot != block.Header.MerkleRoot {
        return errors.New("invalid merkle root")
    }

    // 5. Verify each transaction
    tempUTXO := bc.utxoSet.Clone()  // Don't modify real UTXO set yet
    for i, tx := range block.Transactions {
        if i == 0 {
            // Coinbase transaction has special rules
            if err := bc.ValidateCoinbase(tx, block); err != nil {
                return err
            }
        } else {
            if err := tx.Verify(tempUTXO); err != nil {
                return fmt.Errorf("tx %d: %w", i, err)
            }
        }
        tempUTXO.Apply(tx)
    }

    return nil
}

Chain Reorganization

func (bc *Blockchain) Reorganize(newTip *Block) error {
    bc.mu.Lock()
    defer bc.mu.Unlock()

    // Find the common ancestor
    currentChain := bc.GetAncestors(bc.tip, 1000)
    newChain := bc.GetAncestors(newTip.Header.Hash(), 1000)

    forkPoint := findCommonHash(currentChain, newChain)
    if forkPoint == (Hash{}) {
        return errors.New("no common ancestor found")
    }

    // Blocks to disconnect (current tip back to fork)
    disconnectBlocks := bc.GetBlocksAfter(forkPoint, bc.tip)

    // Blocks to connect (fork to new tip)
    connectBlocks := bc.GetBlocksAfter(forkPoint, newTip.Header.Hash())

    // Collect transactions from disconnected blocks for mempool
    var orphanedTxs []*Transaction
    for _, block := range disconnectBlocks {
        orphanedTxs = append(orphanedTxs, block.Transactions[1:]...) // Skip coinbase
    }

    // Rollback UTXO set
    for i := len(disconnectBlocks) - 1; i >= 0; i-- {
        bc.utxoSet.Disconnect(disconnectBlocks[i])
    }

    // Apply new blocks
    for _, block := range connectBlocks {
        if err := bc.ValidateBlock(block); err != nil {
            // Revert: this shouldn't happen if new chain was pre-validated
            return fmt.Errorf("reorganization failed: %w", err)
        }
        bc.utxoSet.Connect(block)
    }

    // Update chain tip
    bc.tip = newTip.Header.Hash()
    bc.height = bc.GetBlockHeight(bc.tip)

    // Re-add orphaned transactions to mempool if still valid
    for _, tx := range orphanedTxs {
        bc.mempool.Add(tx, bc.utxoSet)
    }

    return nil
}

Phased Implementation Guide

Phase 1: Core Data Types (Days 1-3)

Goal: Define and test the fundamental data structures.

Tasks:

  1. Define Hash type (32-byte array with hex encoding)
  2. Define Block and BlockHeader structures
  3. Define Transaction, TxInput, and TxOutput structures
  4. Implement serialization/deserialization for all types
  5. Implement block header hashing (double SHA-256)
  6. Create genesis block with hardcoded values

Validation:

func TestBlockHash(t *testing.T) {
    genesis := CreateGenesisBlock()
    hash := genesis.Header.Hash()

    // Genesis hash should be consistent
    expected := MustDecodeHash("000000000019d6689c085ae165831e93...")
    if hash != expected {
        t.Errorf("Genesis hash mismatch: got %x", hash)
    }
}

func TestSerialization(t *testing.T) {
    tx := &Transaction{...}
    data := tx.Serialize()
    recovered, err := DeserializeTransaction(data)

    if !reflect.DeepEqual(tx, recovered) {
        t.Error("Transaction serialization round-trip failed")
    }
}

Hints if stuck:

  • Use encoding/binary with BigEndian for all multi-byte integers
  • Double SHA-256 means: SHA256(SHA256(data))
  • Genesis block can have any prev_hash (often all zeros)
  • Test serialization round-trips thoroughly

Phase 2: Merkle Tree (Days 4-5)

Goal: Implement Merkle tree for transaction commitment.

Tasks:

  1. Compute leaf hashes from transactions (double SHA-256 of serialized tx)
  2. Implement recursive tree construction
  3. Handle odd number of leaves (duplicate the last one)
  4. Verify Merkle root matches expected for test blocks

Validation:

func TestMerkleRoot(t *testing.T) {
    // Block 170 from Bitcoin (first non-coinbase transaction)
    txids := []Hash{
        MustDecodeHash("b1fea52486ce0c62bb442b530a3f0132b826c74e..."),
        MustDecodeHash("f4184fc596403b9d638783cf57adfe4c75c605f6..."),
    }

    root := ComputeMerkleRoot(txids)
    expected := MustDecodeHash("7dac2c5666815c17a3b36427de37bb9d...")

    if root != expected {
        t.Errorf("Merkle root mismatch")
    }
}

Hints if stuck:

  • If odd number of nodes, duplicate the last one
  • Concatenate two hashes, then double-SHA256 the result
  • Order matters: left ย  right
  • Single transaction: Merkle root equals transaction hash

Phase 3: UTXO Set (Days 6-9)

Goal: Track unspent outputs and validate spending.

Tasks:

  1. Design UTXO storage (key: txid+index, value: output data)
  2. Implement Add (new transaction outputs)
  3. Implement Spend (mark output as spent)
  4. Implement Get (lookup specific UTXO)
  5. Implement GetByAddress (find UTXOs for an address)
  6. Implement balance calculation

Validation:

func TestUTXOSpend(t *testing.T) {
    utxo := NewUTXOSet()

    // Add a UTXO
    txid := randomHash()
    utxo.Add(txid, 0, &TxOutput{Amount: 1000, PubKeyHash: addr1})

    // Verify it exists
    output, exists := utxo.Get(txid, 0)
    assert(exists && output.Amount == 1000)

    // Spend it
    utxo.Spend(txid, 0)

    // Verify it's gone
    _, exists = utxo.Get(txid, 0)
    assert(!exists)
}

Hints if stuck:

  • Use a map with composite key: map[string]TxOutput where key is txid:index
  • Keep a separate index by address for fast balance lookups
  • Consider using an interface for storage to allow disk-backed implementations later

Phase 4: Transaction Validation (Days 10-14)

Goal: Full transaction validation including signatures.

Tasks:

  1. Implement ECDSA key generation (secp256k1)
  2. Implement address generation (Hash160 + Base58Check)
  3. Implement transaction signing
  4. Implement signature verification
  5. Implement full transaction validation (format + signatures + amounts)

Validation:

func TestTransactionSignVerify(t *testing.T) {
    // Create key pair
    privKey, pubKey := GenerateKeyPair()
    addr := PubKeyToAddress(pubKey)

    // Create UTXO for this address
    utxo := NewUTXOSet()
    fundingTx := randomHash()
    utxo.Add(fundingTx, 0, &TxOutput{
        Amount:     5000,
        PubKeyHash: AddrToPubKeyHash(addr),
    })

    // Create and sign transaction
    tx := &Transaction{
        Inputs: []TxInput{{
            PrevTxHash:  fundingTx,
            OutputIndex: 0,
        }},
        Outputs: []TxOutput{{
            Amount:     4900,  // 100 satoshi fee
            PubKeyHash: randomPubKeyHash(),
        }},
    }

    SignTransaction(tx, privKey)

    // Verify
    err := tx.Verify(utxo)
    assert(err == nil)
}

Hints if stuck:

  • Signature hash excludes the signature itself (use placeholder)
  • Hash160 = RIPEMD160(SHA256(data))
  • Base58Check adds a version byte and 4-byte checksum
  • Use existing ECDSA library (crypto/ecdsa or btcec)

Phase 5: Blockchain State (Days 15-20)

Goal: Manage chain state, add blocks, handle basic forks.

Tasks:

  1. Implement blockchain initialization with genesis block
  2. Implement AddBlock with full validation
  3. Track chain tip and height
  4. Implement GetBlockByHash and GetBlockByHeight
  5. Detect when a new block extends a non-tip block (fork)
  6. Implement simple chain selection (height comparison)

Validation:

func TestBlockchainAdd(t *testing.T) {
    bc := NewBlockchain()
    assert(bc.Height() == 0)  // Genesis

    block1 := CreateBlock(bc.Tip(), []Transaction{coinbase})
    err := bc.AddBlock(block1)
    assert(err == nil)
    assert(bc.Height() == 1)

    // Invalid block (wrong prev_hash)
    badBlock := CreateBlock(randomHash(), []Transaction{coinbase})
    err = bc.AddBlock(badBlock)
    assert(err != nil)  // Should reject
}

func TestForkDetection(t *testing.T) {
    bc := NewBlockchain()

    // Add blocks 1, 2, 3
    b1 := CreateBlock(bc.Tip(), ...)
    bc.AddBlock(b1)
    b2 := CreateBlock(bc.Tip(), ...)
    bc.AddBlock(b2)
    b3 := CreateBlock(bc.Tip(), ...)
    bc.AddBlock(b3)

    // Create fork from block 2
    b3alt := CreateBlock(b2.Hash(), ...)  // Different from b3

    // This should be accepted but not become tip yet
    err := bc.AddBlock(b3alt)
    assert(err == nil)
    assert(bc.Tip() == b3.Hash())  // Still on main chain
}

Hints if stuck:

  • Store blocks in a map by hash for O(1) lookup
  • Store a separate map of height -> hash for height queries
  • Fork detection: if a valid blockโ€™s prev_hash is not the current tip
  • For now, just track forks; reorganization comes in Phase 6

Phase 6: Chain Reorganization (Days 21-25)

Goal: Handle chain reorganizations when a longer fork is found.

Tasks:

  1. Implement UTXO set rollback (undo block)
  2. Implement ancestor chain tracing
  3. Implement fork detection with depth comparison
  4. Implement full reorganization logic
  5. Test with multi-block reorgs

Validation:

func TestReorganization(t *testing.T) {
    bc := NewBlockchain()

    // Build main chain: G <- A <- B <- C
    blockA := CreateBlock(bc.Tip(), txsA)
    bc.AddBlock(blockA)
    blockB := CreateBlock(bc.Tip(), txsB)
    bc.AddBlock(blockB)
    blockC := CreateBlock(bc.Tip(), txsC)
    bc.AddBlock(blockC)

    assert(bc.Height() == 3)

    // Build longer fork: G <- A <- B' <- C' <- D'
    blockBp := CreateBlock(blockA.Hash(), txsBp)
    bc.AddBlock(blockBp)  // Fork, same height as B

    blockCp := CreateBlock(blockBp.Hash(), txsCp)
    bc.AddBlock(blockCp)  // Fork, same height as C

    blockDp := CreateBlock(blockCp.Hash(), txsDp)
    bc.AddBlock(blockDp)  // Longer! Should trigger reorg

    // Verify reorg happened
    assert(bc.Tip() == blockDp.Hash())
    assert(bc.Height() == 4)

    // Verify UTXO set reflects new chain
    // (transactions in B, C should be undone, B', C', D' applied)
}

Hints if stuck:

  • To undo a block: for each tx, remove outputs and restore inputs
  • Store spent outputs somewhere (or recompute from chain) for rollback
  • Find common ancestor by walking back from both tips until they match
  • Validate the entire new chain from fork point before committing

Phase 7: Mempool (Days 26-30)

Goal: Implement transaction mempool for unconfirmed transactions.

Tasks:

  1. Implement mempool add with validation
  2. Implement double-spend detection within mempool
  3. Implement transaction removal when included in block
  4. Implement orphan handling (tx spending unconfirmed output)
  5. Implement fee-based prioritization
  6. Implement mempool size limits and eviction

Validation:

func TestMempoolDoubleSpend(t *testing.T) {
    utxo := NewUTXOSet()
    mempool := NewMempool()

    // Fund an address
    utxo.Add(fundingTx, 0, &TxOutput{Amount: 1000, ...})

    // First transaction spending the UTXO
    tx1 := CreateTx(fundingTx, 0, recipient1, 900)
    Sign(tx1, privKey)
    err := mempool.Add(tx1, utxo)
    assert(err == nil)

    // Second transaction trying to spend same UTXO
    tx2 := CreateTx(fundingTx, 0, recipient2, 900)
    Sign(tx2, privKey)
    err = mempool.Add(tx2, utxo)
    assert(err != nil)  // Should reject as double-spend
}

Hints if stuck:

  • Track which UTXOs are โ€œreservedโ€ by mempool transactions
  • When block is mined, remove all included transactions
  • Handle transaction chains within mempool (parent must be included first)
  • Simple eviction: remove lowest fee-rate transactions

Phase 8: P2P Networking (Days 31-40)

Goal: Implement peer-to-peer communication.

Tasks:

  1. Define message protocol (VERSION, VERACK, INV, GETDATA, BLOCK, TX)
  2. Implement TCP server accepting connections
  3. Implement peer handshake (version exchange)
  4. Implement inventory announcement (INV messages)
  5. Implement block/transaction request and response
  6. Implement gossip (relay received blocks/transactions to peers)
  7. Implement initial block download (sync from peers)
  8. Handle peer disconnection and reconnection

Validation:

func TestPeerHandshake(t *testing.T) {
    node1 := NewNode(port1)
    node2 := NewNode(port2)

    go node1.Start()
    go node2.Start()

    node1.ConnectTo("localhost:" + port2)

    time.Sleep(100 * time.Millisecond)

    assert(node1.PeerCount() == 1)
    assert(node2.PeerCount() == 1)
    assert(node1.Peers()[0].Version == node2.Version)
}

func TestBlockPropagation(t *testing.T) {
    // Create 3 connected nodes
    nodes := createConnectedNodes(3)

    // Node 0 mines a block
    block := nodes[0].MineBlock()

    // Wait for propagation
    time.Sleep(500 * time.Millisecond)

    // All nodes should have the block
    for i, node := range nodes {
        assert(node.HasBlock(block.Hash()), "Node %d missing block", i)
    }
}

Hints if stuck:

  • Use a simple length-prefixed message format: [4-byte length][message type][payload]
  • Keep a list of โ€œseenโ€ message hashes to prevent gossip loops
  • Use goroutines for each peer connection
  • Implement timeouts for all network operations
  • For initial sync: request headers first, then full blocks

Testing Strategy

Unit Tests

// Test individual functions in isolation
func TestHashComputation(t *testing.T) {
    input := []byte("hello")
    hash := DoubleSHA256(input)
    expected := MustDecodeHash("9595c9df90075148eb06860365df33584b75...")
    assert.Equal(t, expected, hash)
}

func TestMerkleProof(t *testing.T) {
    txids := []Hash{tx1, tx2, tx3, tx4}
    root := ComputeMerkleRoot(txids)
    proof := GenerateMerkleProof(txids, 2)  // Proof for tx3

    valid := VerifyMerkleProof(tx3, proof, root)
    assert.True(t, valid)
}

Integration Tests

// Test multiple components working together
func TestFullTransactionLifecycle(t *testing.T) {
    bc := NewBlockchain()
    mempool := NewMempool()

    // 1. Create wallet
    privKey, pubKey := GenerateKeyPair()

    // 2. Mine a block to get coins (coinbase to our address)
    block := MineBlock(bc, PubKeyToAddress(pubKey))
    bc.AddBlock(block)

    // 3. Create spending transaction
    tx := CreateTx(block.Transactions[0].TxID(), 0, recipientAddr, 25)
    Sign(tx, privKey)

    // 4. Add to mempool
    err := mempool.Add(tx, bc.UTXOSet())
    assert.NoError(t, err)

    // 5. Mine block including this transaction
    block2 := MineBlock(bc, minerAddr, mempool.GetTransactions(10))
    bc.AddBlock(block2)

    // 6. Verify UTXO changes
    _, exists := bc.UTXOSet().Get(tx.TxID(), 0)
    assert.True(t, exists)  // New UTXO exists

    _, exists = bc.UTXOSet().Get(block.Transactions[0].TxID(), 0)
    assert.False(t, exists)  // Original UTXO spent
}

Fuzz Tests

func FuzzTransactionDeserialization(f *testing.F) {
    // Seed with valid transaction bytes
    f.Add(validTxBytes)

    f.Fuzz(func(t *testing.T, data []byte) {
        tx, err := DeserializeTransaction(data)
        if err != nil {
            return  // Invalid input is expected
        }

        // If we deserialized, serialization should round-trip
        reserialized := tx.Serialize()
        tx2, err := DeserializeTransaction(reserialized)
        assert.NoError(t, err)
        assert.Equal(t, tx, tx2)
    })
}

Network Tests

func TestNetworkPartitionRecovery(t *testing.T) {
    // Create two groups of nodes
    group1 := createConnectedNodes(3)
    group2 := createConnectedNodes(3)

    // Each group mines different blocks (creating a fork)
    for i := 0; i < 5; i++ {
        group1[0].MineBlock()
        group2[0].MineBlock()
    }

    // Let both groups sync internally
    waitForSync(group1)
    waitForSync(group2)

    // Verify groups have different tips
    assert.NotEqual(t, group1[0].Tip(), group2[0].Tip())

    // Reconnect the groups (simulate partition healing)
    connectGroups(group1, group2)

    // Wait for convergence
    waitForSync(append(group1, group2...))

    // All nodes should agree on the same tip now
    tip := group1[0].Tip()
    for _, node := range append(group1, group2...) {
        assert.Equal(t, tip, node.Tip())
    }
}

Common Pitfalls & Debugging

Pitfall 1: Endianness Confusion

Problem: Bitcoin uses little-endian for most fields but big-endian for hashes when displayed.

Symptom: Hashes donโ€™t match expected values, blocks appear invalid.

Solution:

// When displaying/comparing hashes, reverse the bytes
func (h Hash) String() string {
    reversed := make([]byte, 32)
    for i := 0; i < 32; i++ {
        reversed[i] = h[31-i]
    }
    return hex.EncodeToString(reversed)
}

// When computing, use natural byte order
func DoubleSHA256(data []byte) Hash {
    first := sha256.Sum256(data)
    return sha256.Sum256(first[:])
}

Pitfall 2: Signature Hash Computation

Problem: The message being signed includes a hash of the transaction, but which parts exactly?

Symptom: Valid transactions fail signature verification.

Solution:

// For each input, clear its signature and pubkey,
// but insert the previous output's pubKeyHash
func (tx *Transaction) SignatureHash(inputIndex int) []byte {
    txCopy := tx.Clone()

    for i := range txCopy.Inputs {
        if i == inputIndex {
            // Insert the scriptPubKey of the output being spent
            prevOutput := getOutput(txCopy.Inputs[i].PrevTxHash, txCopy.Inputs[i].OutputIndex)
            txCopy.Inputs[i].ScriptSig = prevOutput.ScriptPubKey
        } else {
            txCopy.Inputs[i].ScriptSig = nil
        }
    }

    data := txCopy.Serialize()
    data = append(data, 0x01, 0x00, 0x00, 0x00)  // SIGHASH_ALL
    return DoubleSHA256(data)
}

Pitfall 3: UTXO Double-Spend in Same Block

Problem: Two transactions in the same block spend the same UTXO.

Symptom: Block validation passes but chain state becomes inconsistent.

Solution:

func (bc *Blockchain) ValidateBlock(block *Block) error {
    tempUTXO := bc.utxoSet.Clone()
    spentInBlock := make(map[string]bool)  // txid:index -> spent

    for _, tx := range block.Transactions[1:] {  // Skip coinbase
        for _, input := range tx.Inputs {
            key := fmt.Sprintf("%s:%d", input.PrevTxHash, input.OutputIndex)

            // Check if already spent in this block
            if spentInBlock[key] {
                return errors.New("double-spend within block")
            }
            spentInBlock[key] = true

            // Also verify against UTXO set...
        }
        tempUTXO.Apply(tx)
    }
    return nil
}

Pitfall 4: Orphan Block Handling

Problem: Receiving blocks out of order (child before parent).

Symptom: Valid blocks rejected because parent is missing.

Solution:

func (bc *Blockchain) AddBlock(block *Block) error {
    if !bc.HasBlock(block.Header.PrevHash) {
        // Parent not found - this is an orphan
        bc.orphans.Add(block)
        bc.RequestBlock(block.Header.PrevHash)  // Ask peers for parent
        return nil  // Don't reject, just defer
    }

    if err := bc.ValidateBlock(block); err != nil {
        return err
    }

    bc.store.PutBlock(block)
    bc.updateTip(block)

    // Check if any orphans can now be processed
    bc.processOrphans(block.Header.Hash())

    return nil
}

func (bc *Blockchain) processOrphans(parentHash Hash) {
    for _, orphan := range bc.orphans.GetByParent(parentHash) {
        bc.AddBlock(orphan)  // Recursive: may trigger more orphan processing
    }
}

Pitfall 5: Race Conditions in P2P

Problem: Multiple goroutines modifying chain state simultaneously.

Symptom: Inconsistent state, panics, data races.

Solution:

type Blockchain struct {
    mu sync.RWMutex
    // ... other fields
}

func (bc *Blockchain) AddBlock(block *Block) error {
    bc.mu.Lock()
    defer bc.mu.Unlock()
    // ... validation and storage
}

func (bc *Blockchain) Tip() Hash {
    bc.mu.RLock()
    defer bc.mu.RUnlock()
    return bc.tip
}

// Use channels for message passing between peer handlers
type Node struct {
    blockChan chan *Block
    txChan    chan *Transaction
}

func (n *Node) handleBlocks() {
    for block := range n.blockChan {
        n.blockchain.AddBlock(block)
    }
}

Extensions and Challenges

Challenge 1: Implement SPV (Simplified Payment Verification)

Build a light client that only downloads block headers and verifies transactions using Merkle proofs:

  • Store only block headers (80 bytes each)
  • Request Merkle proofs from full nodes
  • Verify transactions without full block data
  • Track UTXO set for watched addresses only

Challenge 2: Add Transaction Scripting

Implement a simple scripting language for transaction conditions:

  • Basic opcodes: OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG
  • Pay-to-PubKey-Hash (P2PKH) scripts
  • Multi-signature scripts (M-of-N)
  • Time-locked transactions (OP_CHECKLOCKTIMEVERIFY)

Challenge 3: Implement Compact Blocks (BIP 152)

Optimize block propagation by sending only short transaction IDs:

  • Nodes share transaction inventory via short IDs
  • When block is mined, send header + short IDs
  • Receiver reconstructs block from mempool
  • Request only missing transactions

Challenge 4: Add Pruning Mode

Implement storage pruning for nodes with limited disk space:

  • Keep only recent N blocks in full
  • Prune transaction data but keep headers
  • Maintain UTXO set as the source of truth
  • Still able to validate new blocks

Challenge 5: Implement a Block Explorer API

Add an HTTP API for querying blockchain data:

  • GET /block/{hash} - Block details
  • GET /tx/{txid} - Transaction details
  • GET /address/{addr}/utxos - Unspent outputs
  • GET /address/{addr}/transactions - Transaction history
  • WebSocket endpoint for new block notifications

Real-World Connections

Bitcoin Core Architecture

Your implementation mirrors Bitcoin Coreโ€™s design:

  • CBlockIndex: Your BlockHeader + height tracking
  • CCoinsViewCache: Your UTXOSet
  • CTxMemPool: Your Mempool
  • CConnman: Your P2P networking layer

Study Bitcoin Coreโ€™s source to see how production systems handle edge cases.

Ethereumโ€™s Differences

Ethereum uses an account model instead of UTXOs:

  • State is a key-value map (address -> {nonce, balance, storage, code})
  • Transactions modify account state
  • State is stored in a Merkle Patricia Trie

Understanding UTXOs helps appreciate why Ethereum chose differently.

Layer 2 Solutions

Your blockchain foundation is what Layer 2 solutions build upon:

  • Lightning Network: Payment channels using Bitcoin transactions
  • Optimistic Rollups: Batch transactions off-chain, commit proofs on-chain
  • Sidechains: Separate chain pegged to main chain via two-way peg

Mining Pools

Real mining uses pools to reduce variance:

  • Miners submit shares (low-difficulty proofs)
  • Pool aggregates work and splits rewards
  • Stratum protocol for miner-pool communication

Project 5 explores proof-of-work in depth.


Resources

Primary References

  1. โ€œMastering Bitcoinโ€ by Andreas Antonopoulos - Comprehensive Bitcoin technical guide
  2. Bitcoin Developer Guide - bitcoin.org/en/developer-guide
  3. Bitcoin Improvement Proposals (BIPs) - github.com/bitcoin/bips
  4. learnmeabitcoin.com - Excellent visual explanations of Bitcoin internals

Code References

  1. btcd - Go Bitcoin implementation: github.com/btcsuite/btcd
  2. Bitcoin Core - Reference implementation: github.com/bitcoin/bitcoin
  3. tiny-bitcoin - Educational minimal implementation

Supplementary Reading

  1. โ€œBitcoin and Cryptocurrency Technologiesโ€ by Narayanan et al. - Academic treatment
  2. โ€œProgramming Bitcoinโ€ by Jimmy Song - Python-based deep dive
  3. Satoshi Nakamotoโ€™s original paper - bitcoin.org/bitcoin.pdf

Self-Assessment Checklist

Before moving to the next project, verify:

Conceptual Understanding

  • I can explain why the blockchain data structure provides tamper evidence
  • I understand the UTXO model and how it differs from account balances
  • I can trace through a transaction from creation to confirmation
  • I understand how forks occur and are resolved
  • I can explain the purpose of each field in the block header

Implementation Verification

  • My genesis block has a consistent, reproducible hash
  • Transactions with invalid signatures are rejected
  • Double-spend attempts are detected and rejected
  • Chain reorganization correctly updates the UTXO set
  • Blocks with invalid Merkle roots are rejected
  • The mempool correctly removes transactions when blocks are mined

Network Functionality

  • Nodes successfully complete handshake
  • New blocks propagate to all connected peers
  • New transactions propagate to all connected peers
  • A new node can sync the full chain from peers
  • Network handles peer disconnection gracefully

Edge Cases

  • Orphan blocks are correctly handled
  • Empty blocks (coinbase only) are valid
  • Maximum block size is enforced
  • Transactions spending unconfirmed outputs work correctly

Whatโ€™s Next?

With a working blockchain, you understand how data is structured and validated. In Project 5: Proof of Work Miner, youโ€™ll add the consensus mechanism that makes creating blocks expensive, preventing spam and enabling decentralized agreement without trusted parties. Youโ€™ll implement difficulty adjustment, nonce searching, and understand why mining is both computationally intensive and economically incentivized.