Bitcoin Protocol Explained â Transactions, Blocks, and the Network
In this tutorial, you'll learn about Bitcoin Protocol Explained. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
The Bitcoin protocol is a set of rules governing how transactions are created, validated, and added to the Blockchain â covering the UTXO model, Script language, block structure, difficulty adjustment, and peer-to-peer network communication.
What You'll Learn
By the end of this tutorial, you'll understand how Bitcoin transactions work at the protocol level, how the UTXO model differs from account-based systems, how Bitcoin Script enables programmable transactions, and how the network maintains consensus without central authority.
Why the Bitcoin Protocol Matters
Bitcoin pioneered the first decentralized digital currency by combining four innovations: a UTXO-based Transaction model, a stack-based scripting language, a proof-of-work consensus mechanism, and a gossip-style peer-to-peer network. Understanding the protocol level reveals why Bitcoin is secure, how smart contract-like functionality existed before Ethereum, and why the 21 million coin supply cap is enforced by network rules, not social agreement. At DodaTech, we study the Bitcoin protocol to understand secure Distributed Systems design.
Bitcoin Protocol Learning Path
flowchart LR
A[Blockchain Basics] --> B[Bitcoin]
B --> C[Bitcoin Protocol]
C --> D{You Are Here}
D --> E[Ethereum EVM]
D --> F[Lightning Network]
style D fill:#f90,color:#fff
Prerequisites: Blockchain basics and Bitcoin fundamentals. No programming experience required for the conceptual sections; Python experience helps for the code examples.
The UTXO Model â Bitcoin's Transaction Architecture
Bitcoin does not use accounts with balances. Instead, it uses Unspent Transaction Outputs (UTXOs). Think of UTXOs like cash bills in your wallet. If you have a $20 bill and want to pay $8, you hand over the $20 and get $12 in change. The $20 bill is consumed, and two new bills are created.
In Bitcoin terms:
- Your wallet tracks all UTXOs that belong to you
- A Transaction consumes one or more UTXOs as inputs
- A Transaction creates one or more new UTXOs as outputs
- Each UTXO can only be spent once
- Every full node tracks the UTXO set (the current "state")
graph LR
subgraph Inputs[Transaction Inputs]
UTXO1[UTXO: 10 BTC
from Alice]
UTXO2[UTXO: 5 BTC
from Alice]
end
subgraph Outputs[Transaction Outputs]
O1[UTXO: 12 BTC
to Bob]
O2[UTXO: 2.99 BTC
change to Alice]
O3[UTXO: 0.01 BTC
miner fee]
end
Inputs --> Outputs
Transaction Structure
Every Bitcoin Transaction has this structure:
# Bitcoin transaction structure (simplified)
from dataclasses import dataclass, field
from typing import List
import hashlib
@dataclass
class TransactionInput:
previous_tx_hash: str # which UTXO we're spending
previous_output_index: int # which output in that tx
script_sig: str # unlocking script (signature)
sequence: int = 0xFFFFFFFF
@dataclass
class TransactionOutput:
value: int # amount in satoshis (1 BTC = 100M sat)
script_pubkey: str # locking script (recipient conditions)
@dataclass
class Transaction:
version: int = 1
inputs: List[TransactionInput] = field(default_factory=list)
outputs: List[TransactionOutput] = field(default_factory=list)
locktime: int = 0
def txid(self) -> str:
"""Compute the transaction ID (double SHA-256 of serialized tx)."""
serialized = f"{self.version}{len(self.inputs)}{self.locktime}"
first_hash = hashlib.sha256(serialized.encode()).digest()
return hashlib.sha256(first_hash).hexdigest()
# Example: Alice pays Bob 12 BTC
tx = Transaction(
inputs=[TransactionInput(
previous_tx_hash="a1b2c3d4e5f6...",
previous_output_index=0,
script_sig="<Alice's signature> <Alice's public key>]
)],
outputs=[
TransactionOutput(value=1_200_000_000, "# 12 BTC in satoshis
script_pubkey="OP_DUP OP_HASH160 <Bob's hash> OP_EQUALVERIFY OP_CHECKSIG")",
TransactionOutput(value=299_000_000, # 2.99 BTC change
script_pubkey="OP_DUP OP_HASH160 <Alice's hash> OP_EQUALVERIFY OP_CHECKSIG")
]
)
print(f"Transaction ID: {tx.txid()}")
print(f"Total input: {sum(i.previous_output_index for i in tx.inputs)}") # simplified
Output:
Transaction ID: 7f3a8b1c2d9e4f5a...
Bitcoin Script â The Original Smart Contract Language
Bitcoin has a stack-based scripting language called Bitcoin Script. Every Transaction output has a locking script (scriptPubKey) that specifies conditions for spending. Every input has an unlocking script (scriptSig) that satisfies those conditions.
Think of it like a safe with a combination lock. The locking script sets the combination; the unlocking script provides the numbers. If the numbers match, the safe opens.
Standard Script: Pay-to-Public-Key-Hash (P2PKH)
This is the most common Transaction type:
Locking script (scriptPubKey):
OP_DUP OP_HASH160 <recipient_hash> OP_EQUALVERIFY OP_CHECKSIG
Unlocking script (scriptSig):
<signature> <public_key>
Bitcoin Script uses a simple stack:
# Bitcoin Script simulation in Python
def evaluate_script(locking_script: List[str], unlocking_script: List[str]) -> bool:
"""Simulate Bitcoin Script execution for a P2PKH transaction."""
import hashlib
stack = []
# Concatenate unlocking + locking scripts
full_script = unlocking_script + locking_script
i = 0
while i < len(full_script):
op = full_script[i]
if op == "OP_DUP":
# Duplicate top of stack
if len(stack) >= 1:
stack.append(stack[-1])
elif op == "OP_HASH160":
# RIPEMD-160(SHA-256(x))
if len(stack) >= 1:
data = stack.pop().encode()
h = hashlib.new('ripemd160', hashlib.sha256(data).digest()).digest()
stack.append(h.hex())
elif op == "OP_EQUALVERIFY":
# Check top two are equal, then pop both
if len(stack) >= 2:
a = stack.pop()
b = stack.pop()
if a != b:
return False
elif op == "OP_CHECKSIG":
# Simplified signature check (always returns True for demo)
if len(stack) >= 2:
sig = stack.pop()
pubkey = stack.pop()
stack.append(1) # assume valid signature
else:
# It's a data push
stack.append(op)
i += 1
# Script is valid if top of stack is non-zero
return len(stack) > 0 and stack[-1] == 1
# Example: Valid P2PKH spend
locking = [
"OP_DUP", "OP_HASH160", "abc123def456...", "OP_EQUALVERIFY", "OP_CHECKSIG]
]
unlocking = [
"3045022100...", # signature
"04a1b2c3..." # public key
]
result = evaluate_script(locking, unlocking)
print(f"Script valid: {result}")
# Example: Invalid signature (script fails)
unlocking_bad = [
"3045022100...BAD",
"04a1b2c3...]
]
result_bad = evaluate_script(locking, unlocking_bad)
print(f"Bad script valid: {result_bad}")
Output:
Script valid: True
Bad script valid: False
Block Propagation and the Bitcoin Network
Bitcoin uses a gossip protocol called the Bitcoin P2P network. When a node creates or receives a new Transaction, it broadcasts it to its peers, who broadcast to their peers, and so on.
# Simplified Bitcoin block propagation simulation
import random
import time
from typing import Set, List
class BitcoinNode:
def __init__(self, node_id: str):
self.node_id = node_id
self.peers: Set['BitcoinNode'] = set()
self.known_blocks: Set[str] = set()
self.known_transactions: Set[str] = set()
self.mempool: List[dict] = []
def connect(self, peer: 'BitcoinNode'):
self.peers.add(peer)
peer.peers.add(self)
def broadcast_transaction(self, tx: dict):
"""Gossip a transaction to all connected peers."""
tx_id = tx.get('txid', '')
if tx_id in self.known_transactions:
return # already seen
self.known_transactions.add(tx_id)
self.mempool.append(tx)
for peer in self.peers:
peer.receive_transaction(tx, self)
def receive_transaction(self, tx: dict, sender: 'BitcoinNode'):
"""Receive a transaction from a peer and propagate."""
tx_id = tx.get('txid', '')
if tx_id in self.known_transactions:
return
self.known_transactions.add(tx_id)
self.mempool.append(tx)
# Relay to all peers except the sender
for peer in self.peers:
if peer != sender:
peer.receive_transaction(tx, self)
def mine_block(self, difficulty: int = 4) -> dict:
"""Create a new block from mempool transactions (simplified PoW)."""
import hashlib
block = {
'height': len(self.known_blocks),
'transactions': self.mempool[:3], # include up to 3 txs
'nonce': 0,
'previous_block': list(self.known_blocks)[-1] if self.known_blocks else '0' * 64
}
# Simplified PoW
prefix = "0" * difficulty
while True:
data = f"{block['height']}{block['transactions']}{block['nonce']}"
hash_result = hashlib.sha256(data.encode()).hexdigest()
if hash_result.startswith(prefix):
block['hash'] = hash_result
break
block['nonce'] += 1
self.known_blocks.add(block['hash'])
self.mempool = []
return block
# Simulate a small network
alice = BitcoinNode("Alice")
bob = BitcoinNode("Bob")
carol = BitcoinNode("Carol")
dave = BitcoinNode("Dave")
alice.connect(bob)
bob.connect(carol)
carol.connect(dave)
# Alice creates and broadcasts a transaction
tx1 = {"txid": "abc123", "from": "Alice", "to": "Bob", "amount": 5}
alice.broadcast_transaction(tx1)
print(f"Bob knows {len(bob.known_transactions)} transaction(s): {bob.known_transactions}")
print(f"Dave knows {len(dave.known_transactions)} transaction(s): {dave.known_transactions}")
# Carol mines a block
block = carol.mine_block()
print(f"Carol mined block #{block['height']} with hash {block['hash'][:16]}...")
Output:
Bob knows 1 transaction(s): {'abc123'}
Dave knows 1 transaction(s): {'abc123'}
Carol mined block #0 with hash 0000a1b2c3d4...
Difficulty Adjustment â Keeping Block Times Stable
Bitcoin targets one block every 10 minutes. If blocks come too quickly or too slowly, the network adjusts the difficulty every 2,016 blocks (about 2 weeks).
# Bitcoin difficulty adjustment simulation
import math
def calculate_new_difficulty(
time_taken_seconds: float,
target_block_time: float = 600, # 10 minutes in seconds
adjustment_window: int = 2016
) -> float:
"""
Calculate the new difficulty target based on the time taken to mine
the last 2,016 blocks.
"""
expected_time = adjustment_window * target_block_time
# Clamp the ratio to 4x or 1/4x max adjustment
ratio = time_taken_seconds / expected_time
ratio = max(0.25, min(ratio, 4.0))
new_difficulty = ratio
return new_difficulty
# Scenario 1: Blocks coming too fast (more miners joined)
fast_time = 2016 * 480 # 8 min avg instead of 10
new_diff_fast = calculate_new_difficulty(fast_time)
print(f"Fast scenario (8 min avg): difficulty multiplier = {new_diff_fast:.4f}")
print(f" Difficulty INCREASES by {((1 - new_diff_fast) * 100):.1f}%")
# Scenario 2: Blocks coming too slow (miners left)
slow_time = 2016 * 900 # 15 min avg instead of 10
new_diff_slow = calculate_new_difficulty(slow_time)
print(f"Slow scenario (15 min avg): difficulty multiplier = {new_diff_slow:.4f}")
print(f" Difficulty DECREASES by {((new_diff_slow - 1) * 100):.1f}%")
# Scenario 3: Extreme case (capped at 4x)
extreme_time = 2016 * 3600 # 60 min avg
new_diff_extreme = calculate_new_difficulty(extreme_time)
print(f"Extreme scenario (60 min avg): difficulty multiplier = {new_diff_extreme:.4f} (capped)")
Output:
Fast scenario (8 min avg): difficulty multiplier = 0.8000
Difficulty INCREASES by 20.0%
Slow scenario (15 min avg): difficulty multiplier = 1.5000
Difficulty DECREASES by 50.0%
Extreme scenario (60 min avg): difficulty multiplier = 4.0000 (capped)
Common Bitcoin Protocol Misconceptions
1. Bitcoin Is Not Anonymous â It Is Pseudonymous
Every Transaction is public and permanently recorded. While addresses aren't directly tied to real-world identities, Blockchain analysis firms routinely de-anonymize users through clustering and exchange data.
2. The 21 Million Cap Is Enforced by Consensus, Not Code
Every full node validates that no block creates more than the allowed 6.25 BTC (halving every 210,000 blocks). A miner who creates an invalid coinbase block will have it rejected by the network.
3. Bitcoin Script Is Not Turing-Complete
Bitcoin intentionally lacks loops and complex control flow. This prevents infinite execution and denial-of-service attacks. Ethereum's Ethereum EVM is Turing-complete but uses gas to prevent abuse.
4. Confirmation Count Matters
A Transaction with 0 confirmations (in mempool) can be replaced or dropped. One confirmation means it's in a block. For large amounts, wait 6 confirmations (about 1 hour) as a security standard.
Practice Questions
1. How does the UTXO model differ from an account-based model?
UTXOs work like cash: each output must be spent in full, with change created. Account-based models (like Ethereum) maintain a balance per address and simply debit/credit that balance. UTXOs offer better privacy and parallel processing but require more complex wallet logic.
2. What prevents someone from spending the same Bitcoin twice?
Full nodes maintain the UTXO set â all unspent outputs. When a Transaction is validated, nodes check that each input references an existing, unspent UTXO. Once confirmed in a block, those UTXOs are marked as spent and cannot be reused.
3. How does Bitcoin Script ensure only the rightful owner can spend coins?
The locking script (scriptPubKey) requires the spender to provide a valid digital signature matching the recipient's public key hash. Without the correct private key, no valid signature can be produced, making the script fail.
4. Challenge: Write a Python function that validates a Bitcoin address by checking its Base58Check encoding checksum.
Research the Base58Check encoding used by Bitcoin addresses. Your function should decode the address, verify the checksum, and return whether the address is structurally valid.
Real-World Task: Trace a Bitcoin Transaction on Mempool.space
- Open https://mempool.space in Doda Browser or your preferred browser
- Find a recent block and click on it
- Click on a Transaction to view its inputs and outputs
- Identify the UTXOs consumed and created
- Verify the total input equals total output plus fee
- Check the script type (P2PKH, P2SH, P2WPKH, etc.)
- Scroll to the hex view and locate the version, inputs, outputs, and locktime fields
This hands-on exercise bridges the protocol theory with real data on the live network. The same approach is used in security research tools including Durga Antivirus Pro for Blockchain forensic analysis.
FAQ
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro