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:
- Understand the blockchain data structure at a fundamental level, including block headers, transaction bodies, hash chaining, and why immutability emerges from these design choices
- Implement the UTXO transaction model used by Bitcoin, grasping how unspent transaction outputs create a verifiable ownership graph without requiring account balances
- Master chain selection algorithms including the longest-chain rule and how forks are detected, validated, and resolved in a decentralized system
- Build a peer-to-peer gossip network that propagates blocks and transactions across nodes, handling network partitions, conflicting data, and Byzantine peers
- 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:
- Proof-of-Work: Makes creating blocks expensive
- Longest-chain rule: Defines which chain is โrealโ
- 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 versionprev_hash: SHA-256 hash of the previous blockโs headermerkle_root: Root of the Merkle tree of transactionstimestamp: Unix timestamp when the block was createddifficulty: The proof-of-work difficulty targetnonce: 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?
- Parallelization: Different UTXOs can be validated independently
- Privacy: Each transaction creates new addresses (though not perfect privacy)
- Simple Validation: Check that inputs exist and are unspent; no balance computation
- 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:
- Consistency: Nodes eventually agree (probabilistic finality)
- Liveness: Valid transactions eventually get included
- Safety: The deeper a block, the harder to reverse (exponentially)
Chain Reorganization
When a node discovers a longer chain, it must:
- Identify the fork point: The common ancestor of current and new chain
- Rollback: Undo all blocks from tip to fork point (return UTXOs, etc.)
- Replay: Apply all blocks from fork point to new tip
- 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:
- Miner finds valid block
- Miner sends
INVmessage to peers with block hash - Peers that donโt have this block send
GETDATA - Miner sends
BLOCKwith full block data - Each receiving peer validates, adds to chain, and repeats step 2
Gossip Process for New Transaction:
- User broadcasts transaction to connected nodes
- Nodes validate transaction against current UTXO set
- If valid, add to mempool and send
INVto peers - 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
- 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
- Transaction Handling
- Create transactions with inputs and outputs
- Validate transaction signatures using ECDSA
- Verify transactions against UTXO set
- Calculate transaction fees
- Serialize/deserialize transactions
- 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
- Mempool Operations
- Accept and validate incoming transactions
- Detect and reject double-spends
- Prioritize transactions by fee rate
- Remove transactions when included in blocks
- Chain Operations
- Validate and add new blocks
- Detect and resolve forks
- Perform chain reorganization
- Identify longest valid chain
- 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:
- Define Hash type (32-byte array with hex encoding)
- Define Block and BlockHeader structures
- Define Transaction, TxInput, and TxOutput structures
- Implement serialization/deserialization for all types
- Implement block header hashing (double SHA-256)
- 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/binarywith 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:
- Compute leaf hashes from transactions (double SHA-256 of serialized tx)
- Implement recursive tree construction
- Handle odd number of leaves (duplicate the last one)
- 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:
- Design UTXO storage (key: txid+index, value: output data)
- Implement Add (new transaction outputs)
- Implement Spend (mark output as spent)
- Implement Get (lookup specific UTXO)
- Implement GetByAddress (find UTXOs for an address)
- 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]TxOutputwhere key istxid: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:
- Implement ECDSA key generation (secp256k1)
- Implement address generation (Hash160 + Base58Check)
- Implement transaction signing
- Implement signature verification
- 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:
- Implement blockchain initialization with genesis block
- Implement AddBlock with full validation
- Track chain tip and height
- Implement GetBlockByHash and GetBlockByHeight
- Detect when a new block extends a non-tip block (fork)
- 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:
- Implement UTXO set rollback (undo block)
- Implement ancestor chain tracing
- Implement fork detection with depth comparison
- Implement full reorganization logic
- 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:
- Implement mempool add with validation
- Implement double-spend detection within mempool
- Implement transaction removal when included in block
- Implement orphan handling (tx spending unconfirmed output)
- Implement fee-based prioritization
- 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:
- Define message protocol (VERSION, VERACK, INV, GETDATA, BLOCK, TX)
- Implement TCP server accepting connections
- Implement peer handshake (version exchange)
- Implement inventory announcement (INV messages)
- Implement block/transaction request and response
- Implement gossip (relay received blocks/transactions to peers)
- Implement initial block download (sync from peers)
- 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
- โMastering Bitcoinโ by Andreas Antonopoulos - Comprehensive Bitcoin technical guide
- Bitcoin Developer Guide - bitcoin.org/en/developer-guide
- Bitcoin Improvement Proposals (BIPs) - github.com/bitcoin/bips
- learnmeabitcoin.com - Excellent visual explanations of Bitcoin internals
Code References
- btcd - Go Bitcoin implementation: github.com/btcsuite/btcd
- Bitcoin Core - Reference implementation: github.com/bitcoin/bitcoin
- tiny-bitcoin - Educational minimal implementation
Supplementary Reading
- โBitcoin and Cryptocurrency Technologiesโ by Narayanan et al. - Academic treatment
- โProgramming Bitcoinโ by Jimmy Song - Python-based deep dive
- 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.