Abstract flowing gradient in deep indigo and blue tones, smooth and luminous, evoking a modern digital learning atmosphere

Computer Science and programming articles. We do not sell courses.

How to Build a Simple Blockchain in Python

A blockchain is a shared record made from ordered blocks. Each block stores data, a timestamp, and a cryptographic fingerprint linked to the previous block. If someone changes an earlier record, its fingerprint changes, breaking the chain that follows. This design supports tamper evidence, although it does not automatically make the stored information true. Learn more about Understanding The Hungarian Algorithm For Assignment Problems.

Building a small blockchain in Python is a useful exercise in hashing, data structures, proof of work, and network validation. The example here is deliberately compact: it is suitable for learning algorithms and software design, not for handling real payments, medical records, or production cryptocurrency.

Understanding Blocks And Hashes

A block is a data structure containing several fields. A basic implementation can store an index, creation time, transactions, a proof-of-work nonce, and the hash of the previous block. Transactions can be simple dictionaries, such as {"sender": "Alice", "receiver": "Ben", "amount": 25}.

A cryptographic hash function converts data of any length into a fixed-length string. Python’s hashlib.sha256() is deterministic, so the same input always creates the same digest. A tiny change in a transaction, timestamp, or nonce produces a completely different result. This relationship between blocks is the central idea behind a linked ledger.

Readers who want to revise the surrounding concepts can browse the algorithms collection, especially material about complexity, searching, and data organisation. A blockchain combines several familiar ideas rather than relying on one mysterious algorithm.

The following function serialises a block consistently before hashing it:

import hashlib
import json

def calculate_hash(block):
    block_data = json.dumps(block, sort_keys=True).encode()
    return hashlib.sha256(block_data).hexdigest()

Sorting dictionary keys matters because equivalent dictionaries should produce the same byte sequence. In a larger system, canonical serialisation rules would need careful specification across programming languages.

Creating The Blockchain Class

The class needs a chain and a collection of pending transactions. The first block is called the genesis block. It has no real predecessor, so its previous hash can be a conventional value such as "0".

import time

class SimpleBlockchain:
    def __init__(self, difficulty=3):
        self.chain = [self.create_genesis_block()]
        self.pending_transactions = []
        self.difficulty = difficulty

    def create_genesis_block(self):
        block = {
            "index": 0,
            "timestamp": time.time(),
            "transactions": [],
            "nonce": 0,
            "previous_hash": "0"
        }
        block["hash"] = calculate_hash(block)
        return block

    def latest_block(self):
        return self.chain[-1]

The difficulty value controls mining. With a difficulty of three, a valid hash must begin with three zero characters. Because SHA-256 outputs appear random, the program must try many nonce values until it finds one that satisfies the rule.

A real blockchain would define its data model more strictly. It might use digital signatures, transaction identifiers, account balances, and a database instead of keeping everything in memory. This small class intentionally keeps the state visible so each step can be inspected in a Python interpreter.

Adding Transactions And Mining

Adding a transaction should not immediately alter the chain. Instead, it places the transaction in a pending list. Mining takes those pending records, creates a candidate block, and searches for a valid nonce. Once the proof of work is found, the block is appended and the pending list is cleared.

    def add_transaction(self, sender, receiver, amount):
        transaction = {
            "sender": sender,
            "receiver": receiver,
            "amount": amount
        }
        self.pending_transactions.append(transaction)

    def mine_pending_transactions(self):
        block = {
            "index": len(self.chain),
            "timestamp": time.time(),
            "transactions": self.pending_transactions,
            "nonce": 0,
            "previous_hash": self.latest_block()["hash"]
        }

        target = "0" * self.difficulty

        while True:
            block["hash"] = calculate_hash(block)
            if block["hash"].startswith(target):
                break
            block["nonce"] += 1

        self.chain.append(block)
        self.pending_transactions = []
        return block

This is a proof-of-work demonstration. Increasing the difficulty makes mining slower because the expected number of attempts grows approximately as (16^d), where (d) is the number of required hexadecimal zeroes. The exact time varies with hardware and the contents of each candidate block.

The loop recalculates the hash after every nonce change. In a production miner, hashing would be heavily optimised and transactions would be selected according to formal rules. Here, the goal is clarity: changing one number changes the block hash until the difficulty condition is met.

Checking Chain Integrity

A chain is useful only if nodes can reject altered or incorrectly linked blocks. Validation should check each block’s stored hash, its connection to the preceding block, and its proof-of-work condition. It should also recalculate the hash from the block’s contents rather than trusting the stored value.

    def is_valid(self):
        target = "0" * self.difficulty

        for position in range(1, len(self.chain)):
            previous = self.chain[position - 1]
            current = self.chain[position]

            stored_hash = current["hash"]
            current_without_hash = {
                key: value for key, value in current.items()
                if key != "hash"
            }

            if stored_hash != calculate_hash(current_without_hash):
                return False

            if current["previous_hash"] != previous["hash"]:
                return False

            if not stored_hash.startswith(target):
                return False

        return True

The implementation above hashes a dictionary without its "hash" field, while the mining method initially hashes the block before adding that field. That is intentional. A block’s hash should describe its content, not include a copy of itself. The genesis block needs the same treatment in a polished implementation, so a helper that consistently excludes "hash" is a sensible refinement.

For example, replacing an amount of 25 with 2500 changes the transaction data and therefore the block digest. The following blocks still point to the old digest, so validation returns False. This is tamper detection, not tamper prevention: someone may edit a local copy, but other nodes can identify the inconsistency.

Running A Small Example

The complete usage pattern is short:

blockchain = SimpleBlockchain(difficulty=3)

blockchain.add_transaction("Alice", "Ben", 25)
blockchain.add_transaction("Ben", "Cara", 7)

mined_block = blockchain.mine_pending_transactions()

print("Mined:", mined_block)
print("Valid:", blockchain.is_valid())

A useful debugging habit is to print the nonce, hash prefix, and number of transactions rather than dumping every field. You can then add another transaction, mine a second block, and inspect how its previous_hash matches the first block’s hash.

The chain is currently a Python list, which makes appending efficient and straightforward. Searching for a particular transaction is linear in the number of stored records, or (O(n)), unless an index is added. The full validation pass is also (O(n)) in the number of blocks, although hashing the contents of large blocks adds a cost based on their data size.

If you enjoy comparing data structures with real-time constraints, this discussion of circular buffers in C offers a useful contrast. A blockchain usually grows by appending records, while a circular buffer deliberately overwrites its oldest entries to maintain a fixed memory limit.

Understanding What The Example Does Not Solve

A hash does not prove who created a transaction. Anyone who can call add_transaction() can claim to be Alice. Real blockchain systems use public-key cryptography: a wallet signs a transaction with a private key, and other nodes verify it with the corresponding public key. Private keys must be protected because the system cannot distinguish a legitimate signature from an authorised-looking copy.

This example also has no peer-to-peer network. There is one local chain, no competing miners, and no consensus mechanism for resolving conflicting histories. A distributed blockchain needs message exchange, node identity, rules for selecting a valid chain, protection against denial-of-service attacks, and recovery after a node disconnects.

Proof of work can consume significant electricity, which is especially relevant when considering data-centre operation in Australia, where energy prices and renewable supply vary between states. A classroom program with difficulty three has negligible impact, but scaling the same mechanism is a different engineering and environmental question.

Handling Australian Data And Deployment

A local prototype should avoid storing personal information directly in an immutable ledger. Australians may use digital wallets, mobile banking, Opal or Myki-style transport services, and online marketplaces every day, but a blockchain record containing names, addresses, or identifiers can create privacy and deletion problems. The Privacy Act 1988 and the Australian Privacy Principles are important considerations when personal information is collected or disclosed.

A safer design may store a random reference or a hash on-chain while keeping the original document in a controlled database. Even then, a hash can create privacy concerns if the original value is easy to guess. Organisations operating in Sydney, Melbourne, Brisbane, or elsewhere should obtain legal and security advice before treating a learning prototype as a customer-facing service.

Australian businesses must also consider consumer protection, financial regulation, taxation, and contractual obligations. A chain that records a café loyalty balance is very different from one that represents an investment, facilitates remittances, or processes payments. The code can demonstrate linked records, but it does not provide compliance with ASIC requirements, anti-money-laundering rules, or the Australian Consumer Law.

Practical Checks Before Extending The Project

Once the basic program works, small tests can expose design errors. Mine an empty block, add a negative amount, modify a previous transaction, and change a block’s index. Decide explicitly whether each case should be accepted. Clear rules are more valuable than adding features quickly.

The following checks provide a sensible learning path:

A next step could be adding digital signatures with Python’s cryptography libraries, then separating transaction validation from block mining. After that, a small Flask API could expose read-only chain data, while persistent storage could replace the in-memory list. Each extension should preserve the central invariant: every accepted block must be internally consistent and correctly linked to its predecessor.

The final program is intentionally modest, but it demonstrates the core mechanics behind a hash-linked ledger: deterministic serialisation, chained references, proof of work, pending transactions, and integrity validation. Understanding those mechanics makes it easier to evaluate larger systems without confusing a teaching model with a secure, decentralised financial network.