Build Your Own Blockchain in Python
Build a working blockchain with SHA-256 hashing and proof of work in Python.
Introduction
30 Second Summary
Every time you send cryptocurrency, buy a digital collectible, or hear about Bitcoin, there is a blockchain working behind the scenes. It is a system designed so that once data is recorded, nobody can secretly go back and change it.
In this project, you will build a fully functional blockchain from scratch in Python. You will write every piece yourself using only Python's built-in libraries, from hashing data with SHA-256 to mining blocks with proof of work and detecting tampering.
What You'll Build
You will run a Python script that mines blocks in real time, prints the full chain to your terminal, and catches data tampering the instant it happens.
By the end of this project, you'll have:
- Mine blocks using proof of work, watching the computer churn through thousands of attempts before finding a hash with leading zeros.
- Print the full chain and trace how each block's SHA-256 hash links back to the previous block, forming an unbreakable sequence.
- Tamper with a block's data and watch the chain's validation reject it. Then try a smarter attack by recalculating hashes and discover first-hand why proof of work makes cheating impractical.
- Secret Mission: Add a transaction ledger with sender, receiver, and amount to turn your chain into a mini cryptocurrency.
Are there any prerequisites?
This project assumes you are comfortable with basic Python concepts like classes, methods, loops, and dictionaries. You should have Python 3.6 or later and VS Code already installed on your machine.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters to you.
Set Up Your Project
The entire blockchain you're about to build lives in a single Python file. No external packages, no complex setup.
Python's standard library already includes everything you need: a hashing function, a serializer, and a clock. In this step, you'll create your project folder and confirm that SHA-256 hashing works.
In this step, get ready to:
- Create the my-blockchain project folder and open it in VS Code.
- Write blockchain.py with three standard library imports.
- Run your first SHA-256 hash to confirm everything works.
Create your project folder
- In VS Code, press Ctrl+` (backtick) to open the built-in terminal. You can also go to Terminal in the menu bar and select New Terminal.
- Move to your Desktop and create the project folder by running these commands:
cd ~/Desktop
mkdir my-blockchain
- Confirm the folder was created by running:
ls
You should see my-blockchain listed in the output.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the my-blockchain folder, and click Open.
Seeing a trust prompt?
VS Code may ask if you trust the authors of the files in this folder. Click Yes, I trust the authors to continue.
VS Code now shows my-blockchain in the Explorer sidebar on the left. The folder is empty for now.
Create blockchain.py and run your first hash
Every block in your blockchain will need three things: a way to create a unique fingerprint (hashing), a way to convert data into a consistent string (serialization), and a timestamp. Python's standard library covers all three.
- In the Explorer sidebar, hover over the my-blockchain folder name and click the New File icon (a page with a plus sign).
- Type blockchain.py and press Enter.
- The file opens in the editor. Add the following code to set up your imports and test your first hash:
# Import hashing, serialization, and timestamp tools
import hashlib
import json
from datetime import datetime
# Quick test: hash a string with SHA-256 and print the fingerprint
print(hashlib.sha256('hello blockchain'.encode()).hexdigest())
What does this code do?
- hashlib gives you access to SHA-256, the hashing algorithm that turns any input into a fixed 64-character hexadecimal fingerprint.
- json converts Python dictionaries into consistent strings. You'll need this later to serialize block data before hashing it.
- datetime records when each block was created.
- The print line feeds the string 'hello blockchain' into SHA-256 and prints the resulting hash. If this works, your entire toolchain is ready.
- Save blockchain.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Before you run this, what do you think the output will look like? How many characters do you expect?
- Open a terminal inside VS Code by pressing Ctrl+` (backtick). If your previous terminal is still open, it should now be in the my-blockchain folder.
- Run your script by entering this command:
python blockchain.py
You should see a 64-character hexadecimal string printed in the terminal. That string is the SHA-256 hash of 'hello blockchain'. No errors, just the fingerprint.
Why is the hash 64 characters?
SHA-256 produces a 256-bit output. Each hexadecimal character represents 4 bits, so 256 bits divided by 4 gives you exactly 64 characters. Every SHA-256 hash is always 64 characters long, no matter how short or long the input is.
Not seeing the hash?
- If you see a "command not found" error for python, try python3 blockchain.py instead. Some systems use python3 as the command name.
- If you see a SyntaxError, check that the quotes and parentheses in the print line match the code above exactly.
- Make sure your terminal is inside the my-blockchain folder. Run pwd to check your current directory.
Help me debug why my blockchain.py script isn't printing a hash
✔️ Awesome, I've got everything!
Great. Double check you've saved blockchain.py and your hash printed successfully.
ⓧ I'd like to double check the full code
Here is the complete blockchain.py file so far. Make sure yours matches:
import hashlib
import json
from datetime import datetime
print(hashlib.sha256('hello blockchain'.encode()).hexdigest())
Your environment is ready and SHA-256 is working. Next up, you'll use this hash function to build a Block class and see how even a single character change produces a completely different fingerprint.
Hash Your First Block
You have a working Python file that can hash a string with SHA-256. Now it's time to use that power for something real.
Every block in a blockchain needs a unique fingerprint. SHA-256 creates a 64-character hex string from any data, and changing even one character in the input produces a completely different output. In this step, you'll build a Block class that gives each block its own hash.
In this step, get ready to:
- Build a Block class that stores data and computes its own SHA-256 hash.
- Test the avalanche effect by creating two blocks with nearly identical data and comparing their hashes.
Build the Block class
Each block in a blockchain holds data and a unique fingerprint computed from that data. Your Block class will store five properties and use them to generate a SHA-256 hash.
- In blockchain.py, find the test line at the bottom of the file:
print(hashlib.sha256('hello blockchain'.encode()).hexdigest())
- Delete this line. It was a quick test from the previous step.
- Below your imports, add the following Block class:
class Block:
def __init__(self, index, data, previous_hash):
# Store each piece of the block's content
self.index = index
self.timestamp = str(datetime.now())
self.data = data
self.previous_hash = previous_hash
self.nonce = 0
# Compute and store this block's unique fingerprint
self.hash = self.compute_hash()
def compute_hash(self):
# Combine all fields into a single sorted JSON string
block_string = json.dumps({
"index": self.index,
"timestamp": self.timestamp,
"data": self.data,
"previous_hash": self.previous_hash,
"nonce": self.nonce
}, sort_keys=True)
# Hash the JSON string with SHA-256 and return the hex digest
return hashlib.sha256(block_string.encode()).hexdigest()
What does this code do?
- __init__ stores the block's index, data, a reference to the previous block's hash, a timestamp, and a nonce (set to 0 for now).
- compute_hash packs all five fields into a JSON string with sort_keys=True so the order is always the same, then runs it through SHA-256.
- self.hash = self.compute_hash() at the end of __init__ means every block gets its fingerprint the moment it is created.
Why sort_keys=True?
Python dictionaries don't guarantee a fixed key order across different runs. sort_keys=True forces json.dumps to sort keys alphabetically, so the same data always produces the same JSON string. Without it, the same block could produce different hashes on different runs.
- Save blockchain.py.
- Confirm your class has no syntax errors by running this command in your terminal:
python blockchain.py
- Your terminal should return to the prompt with no error messages. This confirms Python parsed your Block class correctly.
Seeing a SyntaxError or IndentationError?
- Check that every line inside __init__ and compute_hash is indented with 8 spaces (or two tabs). Python is strict about indentation.
- Make sure the closing }, sort_keys=True) in compute_hash is on its own line, aligned with the opening block_string.
- Confirm you deleted the old print(hashlib.sha256(...)) line completely. Leftover fragments can cause errors.
Help me fix a SyntaxError in my Block class in blockchain.py
Prove the avalanche effect
The avalanche effect is the idea that changing even one character in the input to a hash function produces a completely different output. This is what makes hashing useful for tamper detection.
- At the bottom of blockchain.py, add the following demo code to create two blocks with nearly identical data:
if __name__ == "__main__":
# Create two blocks with nearly identical data
block_a = Block(0, "Hello Blockchain", "0" * 64)
block_b = Block(0, "Hello Blockchain!", "0" * 64)
# Print both hashes to compare
print("Block A hash:", block_a.hash)
print("Block B hash:", block_b.hash)
What does this code do?
- if __name__ == "__main__": ensures the demo code only runs when you execute the file directly, not when another file imports it.
- block_a contains "Hello Blockchain" and block_b contains "Hello Blockchain!". The only difference is a single exclamation mark.
- "0" * 64 creates a string of 64 zeros. This is a placeholder for the previous block's hash, since these blocks aren't part of a chain yet.
- Save blockchain.py.
Before you run this, what do you think will happen? Will the two hashes look similar since the data is almost the same, or will they be completely different?
- Run the file to see the result:
python blockchain.py
You see two completely different 64-character hashes, even though the only difference between the two blocks is a single exclamation mark. That is the avalanche effect in action.
Seeing a NameError or no output?
- If you see NameError: name 'Block' is not defined, make sure the class Block: definition appears above the if __name__ block in your file.
- If you see no output at all, check that the if __name__ == "__main__": line is not indented. It must start at column 0, outside the class.
Help me debug my Block class demo output
✔️ Awesome, I've got everything!
Great. Double check you've saved blockchain.py and move on to the next step.
ⓧ I'd like to double check the full code
Here is the complete blockchain.py file at the end of this step. Compare it to yours line by line.
import hashlib
import json
from datetime import datetime
class Block:
def __init__(self, index, data, previous_hash):
self.index = index
self.timestamp = str(datetime.now())
self.data = data
self.previous_hash = previous_hash
self.nonce = 0
self.hash = self.compute_hash()
def compute_hash(self):
block_string = json.dumps({
"index": self.index,
"timestamp": self.timestamp,
"data": self.data,
"previous_hash": self.previous_hash,
"nonce": self.nonce
}, sort_keys=True)
return hashlib.sha256(block_string.encode()).hexdigest()
if __name__ == "__main__":
block_a = Block(0, "Hello Blockchain", "0" * 64)
block_b = Block(0, "Hello Blockchain!", "0" * 64)
print("Block A hash:", block_a.hash)
print("Block B hash:", block_b.hash)
You now have blocks that can fingerprint their own data. Next up, you'll chain these blocks together so that changing one block breaks every block after it.
Chain Blocks Together
In the last step, you proved that SHA-256 turns block data into a unique fingerprint. A single hash by itself is just a random string, though. It does not protect anything on its own.
The power of blockchain comes from chaining blocks together. Each block stores the previous block's hash inside itself, so changing one block invalidates every block after it. In this step, you will build that chain and see the links for yourself.
In this step, get ready to:
- Create a Blockchain class with a genesis block.
- Add methods to grow and display the chain.
- Run the blockchain and trace the hash links between blocks.
Create the Blockchain class
Every blockchain starts with a special first block called the genesis block. Since no block exists before it, the genesis block uses a placeholder for its previous hash: 64 zeros.
- In blockchain.py, add the following Blockchain class below the Block class and above the if __name__ block:
class Blockchain:
def __init__(self):
# Start with an empty chain and create the first block
self.chain = []
self._create_genesis_block()
def _create_genesis_block(self):
# Block 0 has no real previous block, so use 64 zeros
genesis = Block(0, "Genesis Block", "0" * 64)
self.chain.append(genesis)
def add_block(self, data):
# Link to the previous block by reading its hash
previous_hash = self.chain[-1].hash
new_block = Block(len(self.chain), data, previous_hash)
self.chain.append(new_block)
def print_chain(self):
# Display each block's details
for block in self.chain:
print(f"\n--- Block #{block.index} ---")
print(f" Timestamp: {block.timestamp}")
print(f" Data: {block.data}")
print(f" Nonce: {block.nonce}")
print(f" Previous Hash: {block.previous_hash}")
print(f" Hash: {block.hash}")
What does this code do?
- __init__ creates an empty list called self.chain and immediately calls _create_genesis_block to add the first block.
- _create_genesis_block creates Block 0 with the data "Genesis Block" and a previous hash of 64 zeros. This is the standard starting point since no real previous block exists.
- add_block reads the last block's hash from the chain and passes it as previous_hash to a new Block. This is the link that creates the chain.
- print_chain loops through every block and prints its index, data, nonce, previous hash, and hash.
Why 64 zeros for the genesis block?
A SHA-256 hash is always 64 hexadecimal characters long. Using "0" * 64 as the genesis block's previous hash keeps the format consistent with real hashes. It is the conventional starting point used in blockchain implementations, including Bitcoin.
- Save blockchain.py.
Build and print your chain
The demo code from the last step created standalone blocks to compare hashes. Now you need demo code that uses your new Blockchain class instead.
- In blockchain.py, find the existing demo code at the bottom of the file:
if __name__ == "__main__":
block_a = Block(0, "Hello Blockchain", "0" * 64)
block_b = Block(0, "Hello Blockchain!", "0" * 64)
print("Block A hash:", block_a.hash)
print("Block B hash:", block_b.hash)
- Replace the entire if __name__ block with this code:
if __name__ == "__main__":
# Create a new blockchain and add sample blocks
bc = Blockchain()
bc.add_block("Alice pays Bob 5 coins")
bc.add_block("Bob pays Charlie 2 coins")
bc.add_block("Charlie pays Alice 1 coin")
bc.print_chain()
What does this demo do?
- bc = Blockchain() creates a new blockchain. The genesis block is created automatically inside __init__.
- Each add_block call creates a new block whose previous_hash is set to the last block's hash. That is the chain link.
- print_chain prints all four blocks (genesis + 3) so you can trace the hash linkage from block to block.
- Save blockchain.py.
Before you run this, take a guess. How many blocks will print, and what do you think Block #0's previous hash will look like?
- Run your blockchain by running this command:
python blockchain.py
You see 4 blocks printed (genesis + 3). Block #0's previous hash is 64 zeros. Each block after that has a previous_hash that matches the hash of the block before it.
That matching pattern is the chain. Every block is cryptographically linked to the one before it.
Not seeing 4 blocks?
- Make sure you saved blockchain.py after making both changes (adding the class and replacing the demo code).
- Check that the Blockchain class is placed below the Block class. Python reads top to bottom, so Blockchain needs Block to already be defined.
- Confirm you replaced the old demo code entirely. Having two if __name__ blocks will cause only the first one to run.
Help me debug my blockchain output.
✔️ Awesome, I've got everything!
Great. Double check you have saved blockchain.py and your output shows 4 linked blocks.
ⓧ I'd like to double check the full code
Scroll up and confirm your blockchain.py has each of these sections in order:
- The three imports at the top: hashlib, json, and datetime.
- The Block class with __init__ and compute_hash (unchanged from the last step).
- The Blockchain class with __init__, _create_genesis_block, add_block, and print_chain.
- The new demo code inside if __name__ == "__main__" that creates a Blockchain(), adds 3 blocks, and calls print_chain().
Your blockchain is alive. You can create blocks and see them linked by their hashes. But can you trust those links? In the next step, you will try to tamper with a block's data and find out whether the chain actually catches it.
Break the Chain
Your blockchain links every block to the one before it. That structure looks solid on paper. But does it actually catch tampering?
In this step, you will build a validation method and put your chain to the test. You will tamper with block data, attempt a "smart" attack, and discover a critical vulnerability hiding beneath the surface.
In this step, get ready to:
- Add an is_valid method that checks every block for tampering.
- Tamper with block data and attempt a "smart" attack that recalculates hashes.
- Discover why the chain is still vulnerable without proof of work.
Add a validation method to your blockchain
Right now your blockchain stores data and links blocks together. But there is no guard checking whether anything has changed. You need a method that walks the entire chain and verifies every block is intact.
- In blockchain.py, find the add_block method inside the Blockchain class.
- Add the following is_valid method below it:
def is_valid(self):
for i in range(1, len(self.chain)):
current = self.chain[i]
previous = self.chain[i - 1]
# Check 1: does the stored hash still match the data?
if current.hash != current.compute_hash():
print(f"Block #{current.index}: Hash mismatch! Data has been tampered with.")
return False
# Check 2: does the chain linkage hold?
if current.previous_hash != previous.hash:
print(f"Block #{current.index}: Chain linkage broken! previous_hash does not match.")
return False
print("Blockchain is valid. No tampering detected.")
return True
What does this code do?
The method starts at block 1 (skipping the genesis block) and runs two checks on every block in the chain.
- Hash integrity check: it recomputes the block's hash from its current data and compares it to the stored hash. If someone changed the data, the fresh hash will not match the stored one.
- Chain linkage check: it verifies that each block's previous_hash still matches the actual hash of the block before it. If a hash was recalculated, this link will snap.
Now update the demo section at the bottom of the file so you can test validation on a healthy chain.
- Find the if __name__ == "__main__": block at the bottom of blockchain.py.
- Replace the entire block with the following code:
if __name__ == "__main__":
bc = Blockchain()
bc.add_block("Alice pays Bob 5 coins")
bc.add_block("Bob pays Charlie 2 coins")
bc.add_block("Charlie pays Alice 1 coin")
# Print the full chain
print("\n--- Full Blockchain ---")
bc.print_chain()
# Validate the untouched chain
print("\nValidating blockchain...")
bc.is_valid()
What changed in the demo?
The demo now adds a print header before the chain output and calls is_valid() after printing. This gives you a baseline: validation should pass on a chain nobody has touched.
- Save blockchain.py.
- Run the file to test validation on a clean chain:
python blockchain.py
You should see the 4 blocks printed, followed by the message Blockchain is valid. No tampering detected.
Seeing an error instead?
- Make sure is_valid is indented inside the Blockchain class at the same level as add_block. If it sits outside the class, Python will not find the method.
- Check that the demo code calls bc.is_valid() with parentheses. Without them, Python prints the method object instead of running it.
Still stuck? Ask for help: Why is my is_valid method not working?
Tamper with a block and watch validation catch it
Validation passed on a clean chain. Now the real test: what happens when someone changes a block's data after it has been added?
- In blockchain.py, scroll to the bottom of the if __name__ block.
- Add this tamper test below the bc.is_valid() call:
# Tamper: change the recorded transaction
print("\nTampering with Block #1...")
bc.chain[1].data = "Alice pays Bob 500 coins"
print("Validating after tampering...")
bc.is_valid()
What does this tamper do?
This directly overwrites Block #1's data field, changing the amount from 5 coins to 500 coins. The block's stored hash was computed from the original data, so it no longer matches.
- Save blockchain.py.
Before you run this, what do you think is_valid will report? Will it catch the change, or will the modified data slip through?
- Run the file to find out:
python blockchain.py
After the clean validation passes, you should see Block #1: Hash mismatch! Data has been tampered with.
Your first check caught the tamper. The stored hash no longer matches a fresh hash computed from the modified data. But what if the attacker is smarter?
Attempt a smart attack and discover the vulnerability
A basic tamper was caught because the stored hash did not match the data anymore. A smarter attacker would recalculate the hash after changing the data, so the hash integrity check passes. Will the chain still catch it?
- Add the smart attack code at the very bottom of the if __name__ block, below the tamper test you just added:
# Smart attack: recalculate the tampered block's hash
print("\nSmart attack: recalculating tampered block hash...")
bc.chain[1].hash = bc.chain[1].compute_hash()
print("Validating after smart attack...")
bc.is_valid()
What does the smart attack do?
After changing the data, the attacker calls compute_hash() to generate a new hash that matches the modified data. This should fool the first check (hash integrity). The question is whether the second check (chain linkage) still catches it.
- Save blockchain.py.
Before you run this, think about it: if the attacker recalculated Block #1's hash, will the chain still detect the change? What would Block #2's previous_hash be pointing to?
- Run the file:
python blockchain.py
After the hash mismatch from the basic tamper, you should see a new message: Block #2: Chain linkage broken! previous_hash does not match.
The chain caught the smart attack. Block #2 still stores the original hash of Block #1, and the recalculated hash does not match it.
So the chain is secure, right?
Not yet. Think about what a truly determined attacker could do. They changed Block #1's data and recalculated its hash. The chain linkage to Block #2 broke. But what if they also recalculated Block #2's hash? And Block #3's? And every block after?
Right now, creating a block takes microseconds. An attacker could walk through the entire chain and fix every hash in milliseconds. Blocks are free to create, so there is no cost to cheating. That is the vulnerability.
Not seeing the expected output?
- Make sure the tamper code appears before the smart attack code in the demo section. The smart attack recalculates the hash of data that was already changed by the tamper.
- Check that you are modifying bc.chain[1] (Block #1, the first block after genesis). Block #0 is the genesis block.
- Confirm that the smart attack line reads bc.chain[1].hash = bc.chain[1].compute_hash() with parentheses on compute_hash().
Still stuck? Help me debug my tamper detection output.
✔️ Awesome, I've got everything!
Great. Double check you have saved blockchain.py and your output shows all three validation results: valid, hash mismatch, and chain linkage broken.
ⓧ I'd like to double check the full code
Your blockchain.py should contain the following sections in order. Scroll up and confirm each one is present.
- Three imports at the top: hashlib, json, and datetime.
- The Block class with __init__ and compute_hash (unchanged from the previous step).
- The Blockchain class with __init__, _create_genesis_block, add_block, the new is_valid method, and print_chain.
- The demo section that creates a blockchain, prints and validates it, tampers with Block #1, validates again, performs the smart attack, and validates a third time.
Your chain catches tampering and even catches a smarter attack that recalculates hashes. But you have also uncovered the uncomfortable truth: without any cost to creating blocks, an attacker could re-chain the entire blockchain in milliseconds. Next up, you will close that gap with proof of work.
Secure the Chain with Proof of Work
In Step 4, your is_valid method caught both a basic tamper and a hash-recalculation attack. But you also discovered the critical flaw: an attacker could fix the entire chain in milliseconds because blocks cost nothing to create.
Proof of work solves this by making block creation computationally expensive. To create a valid block, the miner must find a number (called a nonce) that makes the block's hash start with a certain number of leading zeros.
In this step, get ready to:
- Add a mine_block method that requires computational effort to create each block.
- Update the Blockchain class to require mining before any block is added.
- Run the final blockchain to see proof of work in action and confirm tampering is still caught.
Add the mine_block method
Right now, a new block's hash is computed once and accepted immediately. Mining changes this: the block must keep trying different nonce values until it finds a hash that starts with a required number of zeros.
- In blockchain.py, find the compute_hash method inside the Block class.
- Add the following mine_block method directly below compute_hash:
def mine_block(self, difficulty):
# Build the target pattern of leading zeros
target = "0" * difficulty
# Keep incrementing nonce until hash matches the target
while not self.hash.startswith(target):
self.nonce += 1
self.hash = self.compute_hash()
print(f"Block #{self.index} mined! Nonce: {self.nonce} | Hash: {self.hash}")
What does this code do?
- target = "0" * difficulty creates the pattern the hash must match. If difficulty is 4, the target is "0000".
- The while loop increments self.nonce and recomputes the hash on every pass. This is the "work" in proof of work.
- The loop only stops when the hash starts with the right number of zeros. Finding this nonce is pure trial and error, which is what makes it computationally expensive.
- Save blockchain.py.
Nothing calls mine_block yet. The next chunk wires it into the Blockchain class so every block must be mined.
Wire mining into the blockchain
With mine_block ready, you need to set a difficulty level and call mining every time a block is created. You will make three small changes to the Blockchain class.
- In the Blockchain class, find this line:
class Blockchain:
- Add a DIFFICULTY class variable directly below it, so the top of the class looks like this:
class Blockchain:
DIFFICULTY = 4
Why difficulty 4?
With DIFFICULTY = 4, every hash must start with 0000. This typically takes a few thousand nonce attempts and a few seconds on modern hardware. It is enough to feel meaningful without being frustratingly slow.
- In the _create_genesis_block method, find this line:
self.chain.append(genesis)
- Add the following line directly above it, so the genesis block is mined before it is appended:
genesis.mine_block(self.DIFFICULTY)
self.chain.append(genesis)
- In the add_block method, find this line:
self.chain.append(new_block)
- Add the mining call directly above it, so the method now looks like this:
new_block.mine_block(self.DIFFICULTY)
self.chain.append(new_block)
- Save blockchain.py.
Before you run this, think: how do you expect the output to look different from previous runs?
- Run your blockchain by running this command:
python blockchain.py
You should see a burst of new output. Each block now prints a mining confirmation line with its nonce count (typically in the thousands) and a hash that starts with 0000. The script also takes noticeably longer to run because the computer is doing real work for each block.
Script finishes instantly with no mining output?
- Make sure you added genesis.mine_block(self.DIFFICULTY) before self.chain.append(genesis) in _create_genesis_block.
- Make sure you added new_block.mine_block(self.DIFFICULTY) before self.chain.append(new_block) in add_block.
- Check that DIFFICULTY = 4 is directly under class Blockchain: and indented with one level.
- Still stuck? Help me figure out why my blockchain isn't mining blocks.
Update the demo and run the final blockchain
The current demo still includes the smart attack from Step 4. Now that proof of work exists, that attack is no longer the interesting story. Re-mining every subsequent block would take thousands of nonce attempts per block, making it computationally impractical.
- In blockchain.py, find the if __name__ == "__main__": section at the bottom of the file.
- Replace the entire section with the following code:
if __name__ == "__main__":
# Create the blockchain with a mined genesis block
print("Creating blockchain...\n")
bc = Blockchain()
# Mine three new blocks with sample data
print("\nAdding blocks...\n")
bc.add_block("Alice pays Bob 5 coins")
bc.add_block("Bob pays Charlie 2 coins")
bc.add_block("Charlie pays Alice 1 coin")
# Print the full chain
print("\n--- Full Blockchain ---")
bc.print_chain()
# Validate the untampered chain
print("\n\nValidating blockchain...")
bc.is_valid()
# Tamper with a block and check if validation catches it
print("\n\nTampering with Block #1...")
bc.chain[1].data = "Alice pays Bob 500 coins"
print("Validating after tampering...")
bc.is_valid()
What changed in the demo?
- Section headers like "Creating blockchain..." are added so the mining output is easier to follow.
- The smart attack is removed. With proof of work, recalculating one block's hash still breaks chain linkage. Fixing it would mean re-mining every subsequent block, each requiring thousands of nonce attempts.
- The demo now focuses on the clean flow: mine blocks, validate, tamper, validate again.
- Save blockchain.py.
Before you run this, think about what you expect to see. Will the untampered chain pass validation? Will tampering still be caught even though you removed the smart attack code?
- Run the final blockchain by running this command:
python blockchain.py
You should see each block mining with a nonce count (typically in the thousands) and every hash starting with 0000. Validation passes for the untampered chain and fails after tampering with a hash mismatch message.
Why does mining time matter?
Each block took thousands of attempts to mine. An attacker who tampers with one block would need to re-mine that block AND every block after it. With a real blockchain containing millions of blocks, this becomes computationally impossible.
This is the core guarantee of proof of work: tampering is not just detectable, it is economically and practically impractical.
Seeing an error or unexpected output?
- If you see a SyntaxError, check that the if __name__ block is not indented inside the Blockchain class. It should be at the very left edge of the file with no indentation.
- If validation fails on the untampered chain, make sure mine_block is called before self.chain.append in both _create_genesis_block and add_block. The block needs to be mined before it is appended to the chain.
- Still stuck? Help me debug my blockchain proof of work output.
✔️ Awesome, I've got everything!
Double check you have saved blockchain.py and your mining output shows all hashes starting with 0000.
ⓧ I'd like to double check the full code
Your blockchain.py is now the complete file. Scroll through and confirm each section is present, in order:
- Three imports at the top: hashlib, json, and datetime.
- Block class with __init__, compute_hash, and mine_block methods.
- Blockchain class with DIFFICULTY = 4 at the top, and both _create_genesis_block and add_block calling mine_block(self.DIFFICULTY) before self.chain.append.
- is_valid and print_chain methods unchanged from Step 4.
- The if __name__ demo at the bottom that creates a blockchain, adds 3 blocks, prints the chain, validates, tampers with Block #1, and validates again. No smart attack code.
Secret mission
Add a Transaction Ledger
Your blockchain stores plain text data right now. What if each block could track exactly who sent coins to whom, and you could calculate everyone's balance from the chain itself? Turn your blockchain into a mini cryptocurrency.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally using Python's standard library, so there are no ongoing costs.
Resources you used:
- The my-blockchain folder on your Desktop containing blockchain.py.
Keep everything running
No action needed. Choose this if you want to keep experimenting with your blockchain or extend it further.
- Your my-blockchain folder stays on your Desktop. You can reopen it in VS Code and run blockchain.py any time.
- There are no background processes or services running. The script only executes when you run it manually.
Pause - I'll come back to this later
Since this project is a single Python file with no running processes, there is nothing to pause. Your files sit on disk and use negligible storage.
- Close the blockchain.py file and the VS Code window for this project.
- When you are ready to return, open the my-blockchain folder in VS Code and pick up where you left off.
Delete - I don't want to use this again
Remove the project folder entirely to start fresh.
- Close the project in VS Code if it is still open.
- Delete the my-blockchain folder by running the command for your operating system.
macOS
rm -rf ~/Desktop/my-blockchain
Windows
Remove-Item -Recurse -Force ~\Desktop\my-blockchain
- Confirm the folder is gone by checking your Desktop. You should no longer see a my-blockchain folder.
Nice Work!
Nice Work!
Nice work! You just built a fully functional blockchain from scratch in Python, complete with cryptographic hashing, chain linking, proof of work mining, and tamper detection.
You've learned how to:
- Built a Block class that hashes its contents with SHA-256 and chains blocks together via previous hashes, so that any modification to a single block breaks the entire chain.
- Implemented proof of work mining that requires thousands of nonce attempts per block, making it computationally impractical for an attacker to re-mine a tampered chain.
- Created an is_valid method with dual checks for hash integrity and chain linkage. You experienced the vulnerability of costless block creation firsthand and understood why proof of work exists to fix it.
- Secret Mission: Transformed the chain into a transaction ledger with sender, receiver, and amount tracking. Added a pending transaction pool and a get_balance method that derives balances by scanning the full chain.
Ready to quiz yourself?