SECTION 1: LEARNING OBJECTIVES
By the end of this lesson, you will be able to:
-
Define cryptographic hash functions and their key properties.
-
Explain the role of public-key cryptography in blockchain.
-
Understand digital signatures and their use in transaction validation.
-
Describe Merkle trees and their importance in block integrity.
-
Differentiate between symmetric and asymmetric encryption.
-
Apply cryptographic primitives in Python to simulate blockchain operations.
-
Assess security implications of cryptographic weaknesses.
-
Develop a practical understanding of how cryptography secures blockchain.
SECTION 2: CRYPTOGRAPHIC HASH FUNCTIONS
2.1 Definition
A cryptographic hash function is a deterministic algorithm that maps an arbitrary-length input to a fixed-size output (the hash). It must satisfy:
-
Pre-image resistance: given a hash, it’s infeasible to find the input.
-
Second pre-image resistance: given an input, it’s infeasible to find another input with the same hash.
-
Collision resistance: it’s infeasible to find two distinct inputs with the same hash.
-
Avalanche effect: a small change in input produces a drastically different hash.
2.2 Common Hash Functions in Blockchain
| Algorithm | Output Size | Use Case |
|---|---|---|
| SHA-256 | 256 bits | Bitcoin block hashing |
| Keccak-256 | 256 bits | Ethereum (SHA-3 variant) |
| RIPEMD-160 | 160 bits | Bitcoin addresses (combined with SHA-256) |
| BLAKE2 | Variable | Used in newer blockchains (e.g., Zcash) |
2.3 Why Hashing Matters
┌─────────────────────────────────────────────────────────────────────────────┐ │ ROLE OF HASHING IN BLOCKCHAIN │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ BLOCK IDENTIFICATION │ │ │ │ Each block is uniquely identified by its hash. │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ CHAIN INTEGRITY │ │ │ │ The previous block's hash is stored in the next block. │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ DATA INTEGRITY │ │ │ │ Any change to transaction data changes the block hash. │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ PROOF OF WORK │ │ │ │ Miners find a nonce that produces a hash below target. │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ MERKLE TREES │ │ │ │ Hashes of transactions are combined into a single root hash. │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘
SECTION 3: PUBLIC-KEY CRYPTOGRAPHY
3.1 Asymmetric Encryption Basics
Public-key cryptography uses a pair of keys:
-
Private key: kept secret, used for signing and decryption.
-
Public key: shared openly, used for verification and encryption.
In blockchain:
-
The private key gives ownership and control over assets.
-
The public key serves as an address (or is hashed to form an address).
-
Digital signatures prove ownership without revealing the private key.
3.2 Elliptic Curve Cryptography (ECC)
Most blockchains use ECC (e.g., secp256k1 in Bitcoin/Ethereum) because it offers strong security with shorter key lengths:
| Encryption | Key Length | Security Level |
|---|---|---|
| RSA | 2048 bits | 112 bits |
| ECC | 256 bits | 128 bits |
SECTION 4: DIGITAL SIGNATURES
4.1 Signing Process
-
Hash the transaction – create a digest.
-
Sign the hash with the private key to produce a signature.
-
Broadcast the transaction with the signature.
4.2 Verification Process
-
Hash the transaction (same as above).
-
Recover the public key or use the provided public key.
-
Verify signature against the hash and public key.
4.3 Standard Algorithms
-
ECDSA (Elliptic Curve Digital Signature Algorithm) – used in Bitcoin, Ethereum.
-
EdDSA (Edwards-curve Digital Signature Algorithm) – used in newer chains (Solana, Cardano).
SECTION 5: MERKLE TREES
5.1 Structure
A Merkle tree is a binary tree where leaves are hashes of transactions, and each internal node is the hash of its children. The top is the Merkle root, stored in the block header.
┌───────────────┐
│ Merkle Root │
└───────────────┘
│
┌────────────┴────────────┐
│ │
┌────┴────┐ ┌────┴────┐
│ Hash(H1,H2)│ │ Hash(H3,H4)│
└──────────┘ └──────────┘
│ │
┌────┴────┐ ┌────┴────┐
│ │ │ │
┌──┴──┐ ┌──┴──┐ ┌──┴──┐ ┌──┴──┐
│ H1 │ │ H2 │ │ H3 │ │ H4 │
└─────┘ └─────┘ └─────┘ └─────┘
Tx1 Tx2 Tx3 Tx4
5.2 Benefits
-
Efficient verification: a client can verify a transaction is included using only the path (logarithmic proof).
-
Integrity: any change in a transaction changes the root.
-
Compression: only the root is stored in the block header.
SECTION 6: IMPLEMENTATION IN PYTHON
# =================================================================== # MODULE 1, LESSON 2: CRYPTOGRAPHIC FOUNDATIONS OF BLOCKCHAIN # =================================================================== import hashlib import binascii import os from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec, utils from cryptography.hazmat.primitives import serialization from cryptography.hazmat.backends import default_backend from typing import List, Tuple import json import time import matplotlib.pyplot as plt import numpy as np import warnings warnings.filterwarnings('ignore') print("="*70) print("CRYPTOGRAPHIC FOUNDATIONS OF BLOCKCHAIN") print("="*70) # ---------------------------------------------------------------- # PART A: HASH FUNCTIONS DEMONSTRATION # ---------------------------------------------------------------- print("\n" + "-"*60) print("PART A: Hash Functions Demonstration") print("-"*60) def hash_sha256(data: str) -> str: """Compute SHA-256 hash of a string.""" return hashlib.sha256(data.encode('utf-8')).hexdigest() def hash_ripemd160(data: str) -> str: """Compute RIPEMD-160 hash of a string.""" return hashlib.new('ripemd160', data.encode('utf-8')).hexdigest() # Test data test_messages = [ "Hello, Blockchain!", "Hello, Blockchain!?", "Hello, Blockchain." # small change ] print("Hash Function Demonstration:") print("-"*40) for msg in test_messages: sha256_hash = hash_sha256(msg) ripemd_hash = hash_ripemd160(msg) print(f"Message: {msg}") print(f" SHA-256: {sha256_hash[:16]}...") print(f" RIPEMD-160: {ripemd_hash[:16]}...") print() # Avalanche effect demonstration msg1 = "Bitcoin" msg2 = "Bitcoin" # same msg3 = "bitcoin" # case change print("Avalanche Effect (SHA-256):") print("-"*40) print(f"'{msg1}' -> {hash_sha256(msg1)[:20]}...") print(f"'{msg2}' -> {hash_sha256(msg2)[:20]}... (same)") print(f"'{msg3}' -> {hash_sha256(msg3)[:20]}... (different)") # ---------------------------------------------------------------- # PART B: PUBLIC-KEY CRYPTOGRAPHY WITH ECC # ---------------------------------------------------------------- print("\n" + "-"*60) print("PART B: Public-Key Cryptography with ECDSA") print("-"*60) from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature, encode_dss_signature def generate_ec_key_pair(): """Generate a private/public key pair using ECDSA (secp256k1).""" private_key = ec.generate_private_key(ec.SECP256K1()) public_key = private_key.public_key() return private_key, public_key def sign_message(private_key, message: str) -> bytes: """Sign a message with the private key.""" signature = private_key.sign( message.encode('utf-8'), ec.ECDSA(hashes.SHA256()) ) return signature def verify_signature(public_key, message: str, signature: bytes) -> bool: """Verify the signature with the public key.""" try: public_key.verify( signature, message.encode('utf-8'), ec.ECDSA(hashes.SHA256()) ) return True except Exception: return False # Generate key pair private_key, public_key = generate_ec_key_pair() # Serialize public key for display public_key_bytes = public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ) print("Key pair generated (secp256k1).") print(f"Public key (PEM first 30 chars): {public_key_bytes[:30].decode('utf-8')}...") # Sign a transaction message = "Transfer 0.5 BTC from Alice to Bob" signature = sign_message(private_key, message) print(f"\nMessage: {message}") print(f"Signature (hex): {binascii.hexlify(signature).decode('utf-8')[:32]}...") # Verify signature valid = verify_signature(public_key, message, signature) print(f"Signature valid? {valid}") # Try tampering tampered_message = "Transfer 0.5 BTC from Alice to Charlie" valid_tampered = verify_signature(public_key, tampered_message, signature) print(f"Tampered message signature valid? {valid_tampered}") # ---------------------------------------------------------------- # PART C: MERKLE TREE IMPLEMENTATION # ---------------------------------------------------------------- print("\n" + "-"*60) print("PART C: Merkle Tree Implementation") print("-"*60) def compute_merkle_root(transactions: List[str]) -> str: """Compute Merkle root from a list of transaction strings.""" # Hash each transaction leaves = [hash_sha256(tx) for tx in transactions] if len(leaves) == 0: return hash_sha256("empty") # If odd number of leaves, duplicate the last one if len(leaves) % 2 == 1: leaves.append(leaves[-1]) # Build tree bottom-up while len(leaves) > 1: parent_level = [] for i in range(0, len(leaves), 2): combined = leaves[i] + leaves[i+1] parent_hash = hash_sha256(combined) parent_level.append(parent_hash) leaves = parent_level # If odd, duplicate last if len(leaves) % 2 == 1 and len(leaves) > 1: leaves.append(leaves[-1]) return leaves[0] if leaves else hash_sha256("empty") def get_merkle_proof(transactions: List[str], tx_index: int) -> List[Tuple[str, str]]: """Generate a Merkle proof for a transaction at given index. Returns list of (sibling_hash, side) where side is 'left' or 'right'. """ leaves = [hash_sha256(tx) for tx in transactions] if len(leaves) == 0 or tx_index >= len(leaves): return [] proof = [] level = leaves while len(level) > 1: # Find sibling index if tx_index % 2 == 0: # sibling is to the right if exists, else duplicate if tx_index + 1 < len(level): sibling = level[tx_index + 1] side = 'right' else: sibling = level[tx_index] # duplicate itself (odd case) side = 'right' # we treat as right else: sibling = level[tx_index - 1] side = 'left' proof.append((sibling, side)) # Compute parent hash if tx_index % 2 == 0: if tx_index + 1 < len(level): combined = level[tx_index] + level[tx_index + 1] else: combined = level[tx_index] + level[tx_index] else: combined = level[tx_index - 1] + level[tx_index] tx_index = tx_index // 2 # Build next level next_level = [] for i in range(0, len(level), 2): if i+1 < len(level): combined = level[i] + level[i+1] else: combined = level[i] + level[i] next_level.append(hash_sha256(combined)) level = next_level return proof def verify_merkle_proof(tx_hash: str, proof: List[Tuple[str, str]], merkle_root: str) -> bool: """Verify a Merkle proof.""" current_hash = tx_hash for sibling_hash, side in proof: if side == 'left': combined = sibling_hash + current_hash else: # right combined = current_hash + sibling_hash current_hash = hash_sha256(combined) return current_hash == merkle_root # Sample transactions transactions = [ "Alice pays Bob 1 BTC", "Bob pays Charlie 2 BTC", "Charlie pays David 0.5 BTC", "David pays Alice 1.5 BTC", "Eve pays Frank 3 BTC" # odd number ] print("Transactions:") for i, tx in enumerate(transactions): print(f" {i}: {tx}") root = compute_merkle_root(transactions) print(f"\nMerkle Root: {root[:20]}...") # Generate proof for transaction at index 2 proof = get_merkle_proof(transactions, 2) print(f"\nProof for transaction index 2 (size {len(proof)}):") for i, (sibling, side) in enumerate(proof): print(f" Level {i+1}: sibling hash = {sibling[:16]}..., side = {side}") # Verify proof tx_hash = hash_sha256(transactions[2]) is_valid = verify_merkle_proof(tx_hash, proof, root) print(f"\nProof verification result: {is_valid}") # Visualise Merkle tree structure def print_merkle_tree(transactions: List[str], max_levels: int = 3): """Pretty print the Merkle tree.""" leaves = [hash_sha256(tx)[:8] for tx in transactions] if len(leaves) % 2 == 1: leaves.append(leaves[-1]) level = leaves levels = [level] while len(level) > 1: parent_level = [] for i in range(0, len(level), 2): combined = level[i] + level[i+1] parent_hash = hash_sha256(combined)[:8] parent_level.append(parent_hash) levels.append(parent_level) level = parent_level if len(level) % 2 == 1 and len(level) > 1: level.append(level[-1]) # Reverse for display (root first) levels.reverse() print("Merkle Tree Structure (truncated hashes):") for i, lvl in enumerate(levels): indent = " " * i if i == 0: print(f"{indent}Root: {lvl[0]}") else: print(f"{indent}Level {i}: {' '.join(lvl)}") print_merkle_tree(transactions) # ---------------------------------------------------------------- # PART D: CRYPTOGRAPHIC SECURITY METRICS # ---------------------------------------------------------------- print("\n" + "-"*60) print("PART D: Cryptographic Security Metrics") print("-"*60) security_metrics = pd.DataFrame({ 'Security Aspect': [ 'Hash Collision Resistance', 'Pre-image Resistance', 'Private Key Space (secp256k1)', 'Quantum Resistance', 'Key Size (ECC)', 'Key Size (RSA)' ], 'Bitcoin (SHA-256/ECDSA)': [ '~2^128 (collision)', '~2^256', '2^256', 'Vulnerable (Shor)', '256 bits', 'N/A' ], 'Ethereum (Keccak-256/ECDSA)': [ '~2^128', '~2^256', '2^256', 'Vulnerable (Shor)', '256 bits', 'N/A' ], 'Post-Quantum Candidate': [ '~2^256', '~2^256', '2^256 (or larger)', 'Resistant', 'Variable', '2048+ bits' ] }) print("Cryptographic Security Metrics:") print(security_metrics.to_string(index=False)) # ---------------------------------------------------------------- # PART E: ADDRESS GENERATION (Bitcoin-style) # ---------------------------------------------------------------- print("\n" + "-"*60) print("PART E: Bitcoin Address Generation (Simplified)") print("-"*60) def generate_bitcoin_address(public_key_pem: bytes) -> str: """Generate a Bitcoin-style address from a public key (simplified).""" # Step 1: SHA-256 of public key (der format) sha256_hash = hashlib.sha256(public_key_pem).digest() # Step 2: RIPEMD-160 of the SHA-256 hash ripemd160 = hashlib.new('ripemd160') ripemd160.update(sha256_hash) hashed_public_key = ripemd160.digest() # Step 3: Add network byte (0x00 for mainnet) network_byte = b'\x00' + hashed_public_key # Step 4: Double SHA-256 checksum checksum = hashlib.sha256(hashlib.sha256(network_byte).digest()).digest()[:4] # Step 5: Concatenate and base58 encode (simplified: use hex for display) address_bytes = network_byte + checksum # Convert to hex for display (real implementation uses Base58Check) return binascii.hexlify(address_bytes).decode('utf-8') # Generate address from our public key address_hex = generate_bitcoin_address(public_key_bytes) print(f"Generated Bitcoin address (hex): {address_hex[:20]}...") print(f"Length: {len(address_hex)} characters") # ---------------------------------------------------------------- # PART F: COMPARISON OF HASH FUNCTIONS # ---------------------------------------------------------------- print("\n" + "-"*60) print("PART F: Performance Comparison of Hash Functions") print("-"*60) # Benchmark hash functions import time data = "Blockchain" * 1000 # 9KB string iterations = 10000 def benchmark_hash(name, hash_func, data): start = time.time() for _ in range(iterations): hash_func(data) elapsed = time.time() - start return elapsed hash_functions = { 'SHA-256': hashlib.sha256, 'SHA-512': hashlib.sha512, 'RIPEMD-160': lambda x: hashlib.new('ripemd160', x.encode('utf-8')).digest(), 'BLAKE2': hashlib.blake2b, } print(f"Benchmarking {iterations} iterations on {len(data)} bytes of data:") results = {} for name, func in hash_functions.items(): try: elapsed = benchmark_hash(name, lambda x: func(x.encode('utf-8')).hexdigest(), data) results[name] = elapsed print(f"{name:12s}: {elapsed:.4f} seconds") except Exception as e: print(f"{name:12s}: Error - {e}") # Visualise performance if results: fig, ax = plt.subplots(figsize=(8, 4)) names = list(results.keys()) times = list(results.values()) ax.bar(names, times, color='teal', alpha=0.7) ax.set_ylabel('Time (seconds)') ax.set_title('Hash Function Performance') ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('hash_performance.png', dpi=300, bbox_inches='tight') plt.show() print("Performance chart saved as 'hash_performance.png'") # ---------------------------------------------------------------- # PART G: SUMMARY AND RECOMMENDATIONS # ---------------------------------------------------------------- print("\n" + "="*70) print("PART G: Summary and Recommendations") print("="*70) print(""" Cryptographic Foundations of Blockchain – Key Takeaways: 1. Cryptographic hash functions provide data integrity and security. - SHA-256, Keccak-256, RIPEMD-160 are widely used. 2. Public-key cryptography (ECC) enables secure ownership and signatures. - Private key controls assets; public key acts as an address. 3. Digital signatures prove authenticity and integrity without revealing keys. 4. Merkle trees allow efficient verification of transaction inclusion. 5. Address generation involves hashing public keys and adding checksums. 6. Quantum computing poses a future threat to ECC and hashes. Recommendations: - Use established cryptographic libraries (cryptography, hashlib). - Never hardcode private keys; use secure key management. - Understand the security assumptions and limitations. - Stay informed about quantum-resistant algorithms. - Validate all signatures and hashes in production systems. """) print("="*70) print("END OF LESSON 2 – MODULE 1") print("="*70)