Minichain Project
Lab deck 5 and the Minichain Flask app: transactions and the mempool, block structure, SHA-256 hashing, chaining with previous_hash, the genesis block, simplified proof of work, chain validation, the Flask routes, and blockchain network families, types and layers.
Building a conceptual blockchainSearch on YouTube
The deck builds a toy blockchain in Python, called Minichain, to answer five questions:
- What is a blockchain?
- What is a transaction?
- What makes a block?
- How are blocks connected?
- When a transaction happens, where does it go? Directly to the block?
The code is in two files: blockchain.py (the Block and Blockchain classes) and app.py (a Flask web API with an HTML front end). The only dependency is Flask==3.0.3.
pip install -r requirements.txt # Flask==3.0.3
python app.py # serves on http://0.0.0.0:5000TerminologiesSearch on YouTube
- Blockchain: a distributed ledger.
- Transactions: records. "A transaction is simply a record describing a proposed state change or transfer."
TransactionsSearch on YouTube
def add_transaction(self, sender, receiver, amount):
transaction = {
"sender": sender,
"receiver": receiver,
"amount": amount
}
self.pending_transactions.append(transaction)
return transactionA transaction here is a Python dict with three fields. It is not written to a block. It is appended to a waiting list.
Mem poolSearch on YouTube
self.pending_transactions = []pending_transactions plays the role of the mempool (memory pool). A transaction is created first. It is not immediately part of the blockchain. It waits in the mempool until a block is mined.
What is a block?Search on YouTube
class Block:
def __init__(self, index, timestamp, transactions,
previous_hash, nonce=0):
self.index = index
self.timestamp = timestamp
self.transactions = transactions
self.previous_hash = previous_hash
self.nonce = nonce
self.hash = self.calculate_hash()| Field | Meaning |
|---|---|
index | Position of the block in the chain (genesis = 0) |
timestamp | Creation time from time() (Unix seconds) |
transactions | List of transaction dicts taken from the mempool |
previous_hash | Hash of the block before this one |
nonce | Number changed during mining; starts at 0 |
hash | SHA-256 of all the fields above |
Hashing a blockSearch on YouTube
def calculate_hash(self):
block_data = {
"index": self.index,
"timestamp": self.timestamp,
"transactions": self.transactions,
"previous_hash": self.previous_hash,
"nonce": self.nonce
}
encoded_block = json.dumps(block_data, sort_keys=True).encode()
return hashlib.sha256(encoded_block).hexdigest()Steps:
- Put every field except
hashin a dict. - Serialise it to JSON with
sort_keys=True. Sorting keys makes the string, and so the hash, the same every time for the same data. .encode()turns the string into bytes.hashlib.sha256(...).hexdigest()returns a 64-character hex string (256 bits).
The hash is a fingerprint of the block. Change one character of one transaction and the hash changes completely.
Connecting the blocksSearch on YouTube
previous_hash=self.get_latest_block().hashWhen creating a new block, we take the hash of the previous block and store it inside the new block. get_latest_block() returns self.chain[-1]. Because previous_hash is part of the hashed data, each block's hash depends on every block before it.
Genesis blockSearch on YouTube
If every block points to a previous block, where does the chain start?
def create_genesis_block(self):
genesis_block = Block(
index=0,
timestamp=time(),
transactions=[],
previous_hash="0"
)
self.chain.append(genesis_block)The genesis block is block 0. It has no transactions and a placeholder previous_hash of "0". The Blockchain constructor creates it automatically:
class Blockchain:
def __init__(self):
self.chain = []
self.pending_transactions = []
self.difficulty = 3
self.create_genesis_block()Pending transactions and block creationSearch on YouTube
self.pending_transactions.append(transaction) # when a tx arrives
transactions=self.pending_transactions.copy() # when a block is builtThe new block gets a copy of the mempool. After mining, the mempool is emptied (self.pending_transactions = []). Copying matters: clearing the list later must not empty the block's own list.
Who can add the block to the blockchain?Search on YouTube
Anyone could build a block. A blockchain needs a mechanism that makes creating a valid block require computational work. That mechanism is Proof of Work.
Proof of WorkSearch on YouTube
self.difficulty = 3
target = "0" * self.difficulty # "000"A block is valid only if its hash starts with difficulty zeros. With difficulty 3, the hash must start with "000".
NonceSearch on YouTube
In the simplified Proof of Work, the nonce is a value we keep changing to generate different hashes.
def mine_pending_transactions(self):
if len(self.pending_transactions) == 0:
return None
new_block = Block(
index=len(self.chain),
timestamp=time(),
transactions=self.pending_transactions.copy(),
previous_hash=self.get_latest_block().hash
)
target = "0" * self.difficulty
while not new_block.hash.startswith(target):
new_block.nonce += 1
new_block.hash = new_block.calculate_hash()
self.chain.append(new_block)
self.pending_transactions = []
return new_blockThe mining flow from the deck:
Mining is hard (many hash attempts). Checking is easy (one hash). This asymmetry is the core of Proof of Work. Each extra zero of difficulty makes mining about 16 times harder, since each hex digit has 16 values.
Validation of the blockchainSearch on YouTube
def is_chain_valid(self):
for i in range(1, len(self.chain)):
current_block = self.chain[i]
previous_block = self.chain[i - 1]
if current_block.hash != current_block.calculate_hash():
return False
if current_block.previous_hash != previous_block.hash:
return False
if not current_block.hash.startswith("0" * self.difficulty):
return False
return TrueThree checks for every block after genesis:
| Check | Catches |
|---|---|
| Stored hash equals recomputed hash | Data inside the block was edited |
previous_hash equals the previous block's hash | The link between blocks was broken |
| Hash starts with the target zeros | A hash was replaced without doing the work |
Hash, Link, Work: "Honest Ledgers Win".
Flask routesSearch on YouTube
app.py wraps one Blockchain() object in a Flask app:
app = Flask(__name__)
blockchain = Blockchain()| Route | Method | What it does | Status codes |
|---|---|---|---|
/ | GET | Renders templates/index.html (the UI) | 200 |
/transactions | POST | Adds a tx to the mempool from JSON sender, receiver, amount | 201; 400 if a field is missing |
/mine | POST | Mines the pending txs into a new block | 200; 400 "No pending transactions to mine" |
/chain | GET | Returns length and the full chain | 200 |
/pending | GET | Returns the mempool | 200 |
/validate | GET | Returns {"valid": true/false} from is_chain_valid() | 200 |
/tamper | POST | Edits the first tx (and optionally the hash) of a block, for demo | 200; 400 or 404 on bad input |
@app.route("/transactions", methods=["POST"])
def create_transaction():
data = request.get_json()
sender = data.get("sender")
receiver = data.get("receiver")
amount = data.get("amount")
if not sender or not receiver or amount is None:
return jsonify({"error": "Sender, receiver and amount are required"}), 400
transaction = blockchain.add_transaction(sender, receiver, amount)
return jsonify({
"message": "Transaction added successfully",
"transaction": transaction,
"pending_transactions": len(blockchain.pending_transactions)
}), 201The /tamper route refuses the genesis block (index 0), a block index that does not exist, and a block with no transactions. It changes sender, receiver or amount of the first transaction. It can also overwrite block.hash directly.
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)Trying the APISearch on YouTube
curl -X POST localhost:5000/transactions -H "Content-Type: application/json" \
-d '{"sender":"Alice","receiver":"Bob","amount":10}'
curl localhost:5000/pending
curl -X POST localhost:5000/mine
curl localhost:5000/chain
curl localhost:5000/validate # {"valid": true}
curl -X POST localhost:5000/tamper -H "Content-Type: application/json" \
-d '{"block_index":1,"amount":1000}'
curl localhost:5000/validate # {"valid": false}What to do and check in the lab:
- Install Flask and run
python app.py; openhttp://localhost:5000. - Use "Create Transaction" a few times, then look at the pending list.
- Click "Mine" and see the nonce and a hash starting with
000. - View the chain: check each block's
previous_hashequals the hash of the block before. - "Validate" gives valid. Use "Tamper Block" to change an amount, then validate again: invalid, because the recomputed hash differs.
- Tamper by setting a new hash too: the next block's
previous_hashlink now breaks.
Now the real game: blockchain networksSearch on YouTube
Major families of blockchain networksSearch on YouTube
| Network | Native asset | Broad purpose |
|---|---|---|
| Bitcoin | BTC | Digital money / settlement |
| Ethereum | ETH | Smart contracts / general-purpose blockchain |
| Solana | SOL | High-throughput smart-contract platform |
| Cardano | ADA | Smart-contract platform |
| Avalanche | AVAX | Smart contracts / customizable networks |
| Polkadot | DOT | Interoperability / multi-chain ecosystem |
| Cosmos ecosystem | ATOM + individual chain tokens | Interoperability / application-specific chains |
| XRP Ledger | XRP | Payments / settlement |
| Stellar | XLM | Payments / asset transfer |
| Sui | SUI | Smart-contract platform |
| Aptos | APT | Smart-contract platform |
Types of blockchainSearch on YouTube
| Type | Definition | Examples |
|---|---|---|
| Public | Open, permissionless network anyone can join, read or write. No central authority; a distributed network validates transactions. | Bitcoin (original P2P digital currency), Ethereum (smart contracts and dApps), Litecoin (faster alternative to Bitcoin) |
| Private | Restricted network controlled by one organisation. Only approved participants access or validate. Faster and more centralised than public. | Hyperledger Fabric (permissioned chains for internal operations), Corda by R3 (private transaction processing for financial institutions), MultiChain (enterprise supply chain or data sharing) |
| Consortium (federated) | Governed by a group of organisations. Several pre-selected entities share validation. Common in banking and supply chains. | Quorum (JPMorgan, interbank), Energy Web Chain (energy companies, grid and energy trading), We.trade (trade finance, European banks) |
| Hybrid | Mix of public and private: some data stays private in an organisation, some is public or verifiable. | XinFin XDC Network (trade finance and supply chain), Dragonchain (built by Disney; anchors proofs to a public chain), IBM Food Trust (supply-chain tracking) |
Mainnet vs testnetSearch on YouTube
Mainnet is the live production network where real transactions occur and tokens have real value (for example Bitcoin's mainnet). Testnet is a separate parallel network for testing. It mimics mainnet but uses worthless test tokens, often free from a faucet. Developers deploy contracts and catch bugs without risking real money.
| Aspect | Mainnet | Testnet |
|---|---|---|
| Purpose | Real-world use | Development and testing |
| Token value | Real, has monetary value | Fake, no monetary value |
| Stability | Stable, final rules | Frequently reset or updated |
| Users | General public, businesses | Developers, testers |
| Example | Ethereum Mainnet | Sepolia, Goerli (Ethereum testnets) |
Layer 1, Layer 2 and overlay networksSearch on YouTube
Layer 1 (L1) is the base blockchain where transactions are permanently recorded and verified by everyone. Examples: Bitcoin, Ethereum. It confirms and secures every transaction and keeps a permanent, tamper-proof record. It depends on no other network; it is the source of truth. The catch: the whole network checks every transaction, so L1s are slow and expensive under heavy use.
Layer 2 (L2) is a separate system on top of an L1. It handles transactions faster and cheaper but relies on the L1 for final security. Examples: Lightning Network (on Bitcoin), Arbitrum and Polygon (on Ethereum). How it works:
- Users do many transactions on the L2, fast and nearly free.
- The L2 periodically bundles them and sends a short summary to the L1.
- The L1 checks and locks in the summary, making it official and secure.
An overlay network runs on top of an existing blockchain but mainly adds a new feature or use case, not speed. Examples on Bitcoin: Lightning Network (also called an overlay; instant micropayments), Ordinals / BRC-20 (tokens and NFT-like functions), Counterparty (asset trading). Key idea: an overlay does not change the base chain. It uses the L1 as a foundation and adds rules that only overlay-aware software interprets.
Quick self-testSearch on YouTube
Viva: when a user submits a transaction in Minichain, does it go straight into a block?
No. add_transaction appends it to pending_transactions, the mempool. It enters a block only when /mine runs mine_pending_transactions.
List the fields of a Minichain block and say which are hashed.
index, timestamp, transactions, previous_hash, nonce and hash. All except hash itself go into the SHA-256 input.
Why does calculate_hash use json.dumps(..., sort_keys=True)?
To get the same string, and so the same hash, for the same data every time. Without sorted keys, key order could change the bytes and the hash.
What is the genesis block and what is its previous_hash?
The first block (index 0), created in the Blockchain constructor. It has no transactions and previous_hash = "0" because no earlier block exists.
Viva: what does the nonce do in Minichain's Proof of Work?
It is a counter that changes the block data so each attempt gives a new hash. Mining increments it until the hash starts with "0" * difficulty ("000").
Why is pending_transactions.copy() used when creating a block?
The mempool is cleared after mining. The copy keeps the block's own list intact when pending_transactions is reset.
Name the three checks in is_chain_valid.
The stored hash equals the recomputed hash. previous_hash equals the previous block's hash. The hash starts with the difficulty zeros.
Viva: you tamper with the amount in block 1. Which check fails, and why?
The first check. The stored hash was computed from the old data, so recomputing from the edited data gives a different hash.
Which Flask route returns HTTP 201, and which returns 400 when there is nothing to mine?
POST /transactions returns 201 on success. POST /mine returns 400 with "No pending transactions to mine".
Distinguish public, private, consortium and hybrid blockchains with one example each.
Public: open and permissionless (Bitcoin). Private: one organisation controls access (Hyperledger Fabric). Consortium: a group of organisations shares validation (Quorum). Hybrid: some data private, some public (XinFin XDC).
How does a Layer 2 differ from an overlay network?
An L2 aims to make transactions faster and cheaper and settles summaries on the L1. An overlay mainly adds a new feature, such as tokens via Ordinals or BRC-20, without changing the base chain.
- A transaction is a record of a proposed transfer; it waits in the mempool (
pending_transactions) until mined. - A block holds
index,timestamp,transactions,previous_hash,nonceand a SHA-256hashof the rest. - Blocks link through
previous_hash; the genesis block has index 0, no transactions andprevious_hash "0". - Proof of Work: increment the nonce until the hash starts with
"000"(difficulty 3); hard to find, easy to check. - Validation checks hash, link and work for every block; any tampering breaks one of them.
- Flask routes:
/transactions,/mine,/chain,/pending,/validate,/tamperon port 5000. - Chain types: public, private, consortium, hybrid. Mainnet has real value; testnets like Sepolia use faucet tokens.
- L1 is the base chain; L2 adds speed and settles on L1; overlays add features without changing L1.