# First: What Problem Does Bitcoin Actually Solve?
Copy it ten times — perfectly fine
Copy it ten times — it stops being money
# What Is a Blockchain in Its Simplest Form?
# What Is a Hash and Why Is It So Important?
185f8db32271fe25f5...
2cf24dba5fb0a30e26e...
import hashlib
text = "Hello"
hash_value = hashlib.sha256(
text.encode()
).hexdigest()
print(hash_value)
# 185f8db32271fe25f5... (fixed length, looks random)
# But a Hash Alone Is Not Enough — Enter Proof of Work
# What Is a Transaction? Meet the UTXO Model
Alice = 100 and subtract from a balance. But real Bitcoin doesn't use a simple account-balance model. Its core model is UTXO — Unspent Transaction Output: the output of a transaction that hasn't been spent yet.
Alice = 100 BTC, imagine Alice holds several unspent outputs:
# Where Do Transactions Live Inside a Block? The Merkle Tree
# Now Let's Build a Tiny Bitcoin Ourselves
address. This program is for learning — not for building real money.
Step 1 — Hashing helpers
import hashlib
import json
import time
def sha256_hex(data: str) -> str:
return hashlib.sha256(data.encode()).hexdigest()
def canonical(obj) -> str:
return json.dumps(
obj,
sort_keys=True,
separators=(",", ":")
)
canonical function matters: we need the data being hashed to always be serialized in a fixed order, so the same data always produces the same hash.
Step 2 — The Transaction
from dataclasses import dataclass
@dataclass(frozen=True)
class TxInput:
txid: str
output_index: int
@dataclass(frozen=True)
class TxOutput:
address: str
amount: int
class Transaction:
def __init__(self, inputs, outputs):
self.inputs = inputs
self.outputs = outputs
def to_dict(self):
return {
"inputs": [
{"txid": i.txid,
"output_index": i.output_index}
for i in self.inputs
],
"outputs": [
{"address": o.address,
"amount": o.amount}
for o in self.outputs
]
}
@property
def txid(self) -> str:
return sha256_hex(
canonical(self.to_dict())
)
Step 3 — The Merkle Root
class Block:
@staticmethod
def calculate_merkle_root_from_txids(txids):
if not txids:
return sha256_hex("")
level = txids[:]
while len(level) > 1:
if len(level) % 2 == 1:
level.append(level[-1]) # duplicate last if odd
level = [
sha256_hex(
level[i] + level[i + 1]
)
for i in range(0, len(level), 2)
]
return level[0]
Step 4 — The Block and its Header
class Block:
def __init__(self, index, transactions,
previous_hash, timestamp=None):
self.index = index
self.timestamp = (
time.time()
if timestamp is None
else timestamp
)
self.transactions = transactions
self.previous_hash = previous_hash
self.nonce = 0
self.merkle_root = (
self.calculate_merkle_root()
)
self.hash = ""
def header_dict(self):
return {
"index": self.index,
"timestamp": self.timestamp,
"merkle_root": self.merkle_root,
"previous_hash": self.previous_hash,
"nonce": self.nonce
}
def calculate_hash(self):
return sha256_hex(
canonical(self.header_dict())
)
Step 5 — Mining
difficulty = 3, the hash must start with 000:
def mine(self, difficulty):
target = "0" * difficulty
while True:
self.hash = self.calculate_hash()
if self.hash.startswith(target):
return # found a valid block!
self.nonce += 1
Step 6 — Actually Using the UTXO Set
(txid, output_index) → output. When Alice spends an output, it's removed from the set and new outputs are added:
("abc123...", 0) → Alice : 100
("def456...", 0) → Bob : 30
("def456...", 1) → Alice : 70
# The Engine: Wallet, Mining and Full Validation
MiniBitcoin class. It rebuilds the UTXO set from the chain, creates payments (with automatic change), mines pending transactions with a coinbase reward, and fully validates the chain — including double-spend detection:
class MiniBitcoin:
def __init__(self, difficulty=3, reward=50,
genesis_address="Alice",
genesis_amount=100):
self.difficulty = difficulty
self.reward = reward
genesis_tx = Transaction(
[],
[TxOutput(genesis_address, genesis_amount)]
)
self.genesis = Block(
0, [genesis_tx], "0" * 64,
timestamp=0
)
self.genesis.hash = (
self.genesis.calculate_hash()
)
self.chain = [self.genesis]
self.pending = []
def _utxos(self):
# Replay the whole chain and rebuild
# the set of unspent outputs.
utxos = {}
for block in self.chain:
for tx in block.transactions:
for tx_input in tx.inputs:
key = (tx_input.txid,
tx_input.output_index)
utxos.pop(key, None) # spent
for index, output in enumerate(tx.outputs):
utxos[(tx.txid, index)] = output
return utxos
def balance(self, address):
return sum(
output.amount
for output in self._utxos().values()
if output.address == address
)
def create_payment(self, sender, recipient, amount):
if amount <= 0:
raise ValueError("amount must be positive")
utxos = [(key, output)
for key, output
in self._utxos().items()
if output.address == sender]
total = 0
inputs = []
for (txid, index), output in utxos:
inputs.append(TxInput(txid, index))
total += output.amount
if total >= amount:
break
if total < amount:
raise ValueError("insufficient funds")
outputs = [TxOutput(recipient, amount)]
change = total - amount
if change:
outputs.append(TxOutput(sender, change))
tx = Transaction(inputs, outputs)
self.pending.append(tx)
return tx
def mine_pending(self, miner_address):
# Coinbase: the miner's reward transaction
reward_tx = Transaction(
[], [TxOutput(miner_address, self.reward)]
)
block = Block(
len(self.chain),
[reward_tx] + self.pending,
self.chain[-1].hash
)
self._validate_candidate_block(block)
block.mine(self.difficulty)
self.chain.append(block)
self.pending = []
return block
def _validate_candidate_block(self, block):
if block.index != len(self.chain):
raise ValueError("invalid block index")
if block.previous_hash != self.chain[-1].hash:
raise ValueError("invalid previous hash")
if block.merkle_root != block.calculate_merkle_root():
raise ValueError("invalid merkle root")
# First transaction must be coinbase (no inputs)
if block.transactions[0].inputs:
raise ValueError(
"first transaction must be coinbase"
)
utxos = self._utxos()
spent_in_block = set()
for tx in block.transactions[1:]:
input_total = 0
for tx_input in tx.inputs:
key = (tx_input.txid, tx_input.output_index)
if key in spent_in_block:
raise ValueError("double spend detected")
if key not in utxos:
raise ValueError("missing UTXO")
spent_in_block.add(key)
input_total += utxos[key].amount
output_total = sum(
o.amount for o in tx.outputs
)
if output_total > input_total:
raise ValueError("outputs exceed inputs")
return True
is_valid() method walks the entire chain with the same rules — checking genesis, header hashes, Proof of Work, coinbase, and replaying every UTXO — and returns a single boolean. We'll use it for the tamper experiment below.
# Now Let's Actually Use It
coin = MiniBitcoin(difficulty=3, reward=50)
print(coin.balance("Alice"))
# 100 (genesis block gave Alice 100)
coin.create_payment("Alice", "Bob", 30)
# transaction goes into `pending` — not in the chain yet
block = coin.mine_pending("Miner")
# the miner builds a block, does Proof of Work, appends it
print(coin.balance("Alice")) # 70
print(coin.balance("Bob")) # 30
print(coin.balance("Miner")) # 50
# The Fun Part: What If We Tamper With a Block?
Alice → Bob : 30 into Alice → Bob : 999. The transaction data changed → its txid changes → the Merkle Root changes → the block hash changes → the block is no longer valid:
print("Valid:", coin.is_valid())
# Valid: True
# someone quietly rewrites history:
coin.chain[1].transactions[1].outputs[0] = (
TxOutput("Bob", 999)
)
print("Valid:", coin.is_valid())
# Valid: False
Previous Hash = old hash of Block 1. The attacker would have to re-mine Block 1, then Block 2, then every block after it — redoing all the Proof of Work. Hash-chaining combined with Proof of Work is precisely what makes rewriting history expensive.
# Something Crucial Is Still Missing: Cryptography and the Network
address = "Alice". If anyone claims to be Alice, our program believes them. In the real world that's not acceptable — and this is where private keys, public keys and digital signatures enter:
- Block hash matches header
- Hash meets difficulty target
- Previous hash is correct
- No output spent twice
# Bitcoin Is a Stack of Ideas, Not a Single Invention
| Feature | Our version | Real Bitcoin |
|---|---|---|
| Block | ✓ | ✓ |
| Previous Hash | ✓ | ✓ |
| SHA-256 | ✓ | ✓ |
| Merkle Root | ✓ | ✓ |
| Proof of Work | ✓ | ✓ |
| Mining Reward | ✓ | ✓ |
| UTXO | ✓ | ✓ |
| Double Spend Check | ✓ | ✓ |
| Digital Signature | ✕ | ✓ |
| Private/Public Keys | ✕ | ✓ |
| P2P Network | ✕ | ✓ |
| Full Consensus | simplified | ✓ |
| Bitcoin Script | ✕ | ✓ |
| Difficulty Adjustment | ✕ | ✓ |
| Transaction Fees | ✕ | ✓ |
# Why Is Building a Small Version Worth It?
it's in how they fit together.
Simple parts + precise rules = a complex system



