LOADING 0%
// nav_menu.exe
Home Resume Blog Contact Order
English فارسی
~/blog / cryptocurrency / how-bitcoin-works

HOW DOES BITCOIN WORK? LET'S BUILD ONE OURSELVES

Imagine we wanted to build a digital money system without a bank. Ali wants to send 30 units to Reza. Reza must be able to verify the money really came from Ali — and Ali must not be able to spend those same 30 units twice. Bitcoin was designed from scratch to solve exactly this problem. In this article, we take it apart piece by piece and build a small but fully working version of it in Python.

# First: What Problem Does Bitcoin Actually Solve?

The core problem of digital money is not just "transferring funds". Suppose I have 100 digital coins. If I can send a copy of those 100 coins to Ali and the same copy to Reza, I have effectively copied a file. That is the famous Double Spending problem.
A File

Copy it ten times — perfectly fine

file.txt → file2.txt ✓
Money

Copy it ten times — it stops being money

100 → 100 → 100 → ∞ ✗
The traditional solution was a central ledger keeper — a bank:
1
User — wants to send money
2
Bank — checks the central ledger
3
Balance check — does the sender have it?
4
Record — the bank writes the transaction
But Bitcoin takes a different route from the very beginning. Instead of one central authority deciding which transaction is valid, a set of computers — Nodes — maintain a shared public ledger and independently verify its rules. The Bitcoin blockchain is really an ordered public registry of transactions, and every node can keep a copy and validate it.
Key insight: Bitcoin may look like an impossibly complex project — but once you split it into smaller parts, the core idea becomes remarkably understandable. That's exactly what we'll do here.

# What Is a Blockchain in Its Simplest Form?

A blockchain, in its simplest form, is nothing more than a chain of blocks. Each block, besides its own data, holds a value called the Hash of the previous block:
B3
Block 3
Previous Hash = hash(Block 2)
B2
Block 2
Previous Hash = hash(Block 1)
B1
Block 1
Previous Hash = hash(Block 0)
B0
Genesis Block
Previous Hash = 000...0
If someone changes the content of Block 1, its hash changes. Block 2 then points to a hash that no longer matches. The chain is broken and the validity of everything after that point collapses. In real Bitcoin, each block references the previous block's hash in its Header, so tampering with one block affects every block after it.

# What Is a Hash and Why Is It So Important?

A hash is like a digital fingerprint for data. Feed a string into SHA-256 and you get a fixed-length output. Change even one character, and the output changes completely:
sha256("Hello")
185f8db32271fe25f5...
sha256("hello")
2cf24dba5fb0a30e26e...
The crucial idea: a tiny change in input → a massive change in output. In our example we'll use SHA-256:
hash_demo.py
import hashlib

text = "Hello"

hash_value = hashlib.sha256(
    text.encode()
).hexdigest()

print(hash_value)
# 185f8db32271fe25f5... (fixed length, looks random)
In Bitcoin, hashing plays many roles — from transaction IDs, to block structure, to Proof of Work itself.

# But a Hash Alone Is Not Enough — Enter Proof of Work

Suppose I've built a block. If producing a valid hash were free, an attacker could just as freely modify the block and compute a new hash. So Bitcoin doesn't merely say "have a correct hash." It says:
The rule: To produce a valid block, you must perform a specific amount of computational work. The hash of the block header must satisfy a difficulty target — for example, start with a certain number of zeros.
Since we can't choose a hash directly — the output looks random — we vary a number called the Nonce and re-hash each time:
145
83af... — rejected
146
c921... — rejected
147
0000b71...accepted!
That is the core idea of Proof of Work: finding a value that makes the block header hash satisfy a specific difficulty. In Bitcoin this serves as proof that computational power was actually spent. And why vary only the Nonce? Because changing the block's data changes its meaning — the Nonce is the one field built to be changed over and over.
Transactions
+
Previous Hash
+
Timestamp
+
Nonce
SHA-256
goal: hash = 0000................

# What Is a Transaction? Meet the UTXO Model

Now the most important part: transactions. A naive system would say 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.
Instead of saying Alice = 100 BTC, imagine Alice holds several unspent outputs:
60
UTXO #1 → Alice
40
UTXO #2 → Alice
100
Total = Alice's "balance"
If Alice wants to send 30 units, she consumes one or more UTXOs and creates new outputs. Think of a 100-unit banknote being broken into two notes:
UTXO = 100 — consumed entirely
30
→ Bob — the payment
70
→ Alice — the Change
And this is exactly how double spending is prevented: if Alice spends UTXO-A for Bob and then tries to spend the same UTXO-A for Reza, the network rejects the second transaction — because that output is no longer unspent. A specific output can be spent at most once. The Bitcoin developer docs describe this same model: a transaction's input points to a previous transaction's output, and each output can be spent only once.

# Where Do Transactions Live Inside a Block? The Merkle Tree

A block can contain many transactions. But how do we verify the whole set hasn't been tampered with, without re-reading everything? This is where the Merkle Tree comes in. Hash the transactions pairwise, level by level, until a single hash remains — the Merkle Root:
Merkle Root
hash(H12 + H34) — goes into the Block Header
H2
Level 2
H12 = hash(H1+H2)  •  H34 = hash(H3+H4)
H1
Level 1
H1..H4 = hash of each transaction's txid
TX
Transactions
TX1 • TX2 • TX3 • TX4
In Bitcoin, transaction hashes form the Merkle Tree and the root sits in the Block Header. This even allows proving a transaction exists in a block without holding the whole blockchain — the idea described in the original Bitcoin paper as Simplified Payment Verification.

# Now Let's Build a Tiny Bitcoin Ourselves

We're not going to rewrite Bitcoin Core. We're going to build a small version that actually executes the core ideas:
1
Transaction — inputs & outputs
2
UTXO — track unspent money
3
Merkle Root — summarize the block
4
Proof of Work — mine the block
5
Blockchain — link and validate
Simplification alert: In real Bitcoin, ownership of a UTXO is proven with private keys and digital signatures. In our version, to keep the code readable, ownership is just a string address. This program is for learning — not for building real money.

Step 1 — Hashing helpers

mini_bitcoin.py — part 1
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=(",", ":")
    )
The 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

Every transaction has two main parts: Inputs (which previous UTXOs are being spent) and Outputs (who receives the new money):
mini_bitcoin.py — part 2
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

mini_bitcoin.py — part 3
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

Our block header holds: Index, Timestamp, Merkle Root, Previous Hash, Nonce — and the block's hash is computed from exactly this header:
mini_bitcoin.py — part 4
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

Now the fun part. With difficulty = 3, the hash must start with 000:
mini_bitcoin.py — part 5
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
This loop is exactly the idea we described: try nonce 0, 1, 2, ... until the hash satisfies the target. In the real Bitcoin network, difficulty is astronomically beyond a few educational zeros, and it's tuned to keep block production steady.

Step 6 — Actually Using the UTXO Set

To know what money is unspent, we keep a dictionary shaped like (txid, output_index) → output. When Alice spends an output, it's removed from the set and new outputs are added:
Before
("abc123...", 0) → Alice : 100
After
("def456...", 0) → Bob : 30
("def456...", 1) → Alice : 70

# The Engine: Wallet, Mining and Full Validation

Now we put the pieces together in a 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:
mini_bitcoin.py — part 6 (wallet & mining)
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
And the validation rules — the closest thing our little program has to consensus rules:
mini_bitcoin.py — part 7 (validation)
    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
The 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

run.py
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
What did the miner actually do? Roughly this:
1
Pending transactions — Alice → Bob : 30
2
+ Coinbase — Miner : 50
3
Merkle Root — summarize transactions
4
Proof of Work — find the Nonce
5
Append — block joins the chain
Why does the miner get paid? If people must spend real computation to build blocks, there must be an incentive. In Bitcoin, the first transaction of every block is a Coinbase Transaction that awards the block reward (plus fees) to the miner. In real Bitcoin the rules are much stricter — for example, a real coinbase can't be spent for at least 100 blocks.

# The Fun Part: What If We Tamper With a Block?

Suppose someone changes the payment 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:
tamper_test.py
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
And tampering with one block isn't enough anyway. Block 2 still stores 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.
A famous misconception: Blockchain is not magic. Data on a blockchain is not "absolutely unchangeable" — its structure is designed so that changing history has a computational cost, and nodes simply refuse invalid blocks. In Bitcoin, nodes validate blocks against their own consensus rules and follow the chain with the most accumulated Proof of Work.

# Something Crucial Is Still Missing: Cryptography and the Network

In our code, ownership is nothing more than the string 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:
Private Key
Digital Signature
Transaction
Network verifies
writing the name "Alice" is not enough
The other big difference is that our program runs on one computer. Bitcoin is not just a program — it's a network of nodes. Transactions are broadcast, nodes verify them, miners assemble blocks, and found blocks are broadcast again. Every node independently checks: Is the hash correct? Is the Proof of Work correct? Are transactions valid? Any double spends? Is the previous hash correct? Only then: accept.
Hash check
  • Block hash matches header
PoW check
  • Hash meets difficulty target
Link check
  • Previous hash is correct
Double spend
  • No output spent twice

# Bitcoin Is a Stack of Ideas, Not a Single Invention

If we open up the whole system, we're no longer looking at a simple "blockchain" — several pieces are stacked together:
P2P Network
Thousands of nodes, no center
C
Consensus Rules
Independent validation by every node
W
Proof of Work
Computational cost for block production
M
Merkle Tree
Compact summary of a block's transactions
B
Blocks
Hash-chained containers of transactions
U
Transactions & UTXO
Ownership modeled as unspent outputs
T
Wallet
Keys, signatures — proof of ownership
So where does what we built fit in? Here's the honest comparison:
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 Consensussimplified
Bitcoin Script
Difficulty Adjustment
Transaction Fees
What we built is the educational core of the idea — not a full implementation of the Bitcoin protocol. And that distinction matters a lot.

# Why Is Building a Small Version Worth It?

Because when we only say "Bitcoin is a digital currency based on blockchain", we've learned almost nothing. But when we watch with our own eyes as a transaction creates new UTXOs, feeds the Merkle Root, gets mined into a block, and breaks the chain the moment someone tampers with it — the concept suddenly becomes clear.
SHA-256 alone isn't remarkable. Hash chains alone aren't new. Digital signatures alone aren't money. P2P networks alone aren't blockchain. Proof of Work alone isn't Bitcoin. But when these pieces are combined under precise rules, they form a system that can maintain a public, verifiable transaction ledger without a central bank. That's the fascinating engineering part of Bitcoin:
Sometimes a revolutionary technology isn't built from inventing one strange component — it's built from placing existing components together in a way nobody had tried before.
takeaway.txt
Bitcoin's power isn't in its parts —
it's in how they fit together.
Simple parts + precise rules = a complex system