Appendix C: Advanced Blockchain Concepts
Introduction
Section titled “Introduction”This appendix covers advanced blockchain concepts that go beyond Minichain’s core implementation but are important for production systems:
- Block Reorganization: Handling forks in multi-authority chains
- Transaction Receipts: Tamper-proof execution records
- Light Clients: Verifying blockchain state without full chain sync
These topics are optional for understanding Minichain’s basic operation, but valuable for anyone building or studying production blockchains.
B.1 Block Reorganization
Section titled “B.1 Block Reorganization”In PoA with a single authority, reorganizations (reorgs) don’t occur — blocks are final. But in multi-authority PoA or PoS, blocks might be produced simultaneously, creating forks:
Timeline:T=0 Authority A: GenesisT=1 Authority B: Block 1B (parent: Genesis)T=1 Authority C: Block 1C (parent: Genesis) [conflict!]T=2 Authority A sees 1B first → builds Block 2B (parent: 1B)T=2 Authority B sees 1C → now what?Fork Choice Rule: Longest chain (most cumulative work)
Reorg Process:
Step 1: Detect Fork
impl ChainStore { pub fn detect_fork(&self, new_block: &Block) -> Result<Option<u64>> { let our_block = self.get_block_by_height(new_block.header.height)?;
if let Some(our_block) = our_block { if our_block.hash() != new_block.hash() { // Fork detected at this height return Ok(Some(new_block.header.height)); } } Ok(None) }}Step 2: Find Common Ancestor
pub fn find_common_ancestor(&self, height: u64) -> Result<u64> { // Walk backwards until hashes match for h in (0..=height).rev() { let our_block = self.get_block_by_height(h)? .ok_or(StorageError::BlockNotFound)?; let their_block = /* fetch from peer */;
if our_block.hash() == their_block.hash() { return Ok(h); // Common ancestor found } } Ok(0) // Genesis is always common}Step 3: Revert Blocks
pub fn revert_to(&mut self, target_height: u64) -> Result<Vec<Transaction>> { let current_height = self.get_height()?; let mut reverted_txs = Vec::new();
// Revert blocks in reverse order for h in ((target_height + 1)..=current_height).rev() { let block = self.get_block_by_height(h)? .ok_or(StorageError::BlockNotFound)?;
// Collect transactions to re-add to mempool reverted_txs.extend(block.body.transactions.clone());
// Revert state changes (complex - requires state snapshots) self.revert_state_for_block(&block)?;
// Remove block from chain self.delete_block(h)?; }
Ok(reverted_txs)}Step 4: Apply New Chain
// Apply blocks from forkfor block in new_chain_blocks { self.execute_block(&block)?; self.put_block(&block)?;}Step 5: Update Mempool
// Re-add reverted transactionsfor tx in reverted_txs { if !new_chain_includes(&tx) { mempool.add_transaction(tx)?; }}Complete Example:
Initial chain (Authority A's view): Genesis → 1B → 2B → 3B
Authority B broadcasts longer chain: Genesis → 1C → 2C → 3C → 4C
Fork detected at height 1.Common ancestor: Genesis (height 0).
Reorg process: 1. Revert 3B, 2B, 1B (collect txs) 2. Apply 1C, 2C, 3C, 4C 3. Re-add unique txs from 1B/2B/3B to mempool
New chain: Genesis → 1C → 2C → 3C → 4CB.2 Transaction Receipts
Section titled “B.2 Transaction Receipts”Receipts provide tamper-proof records of execution results.
Receipt Structure:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct TransactionReceipt { // Identity pub transaction_hash: Hash, pub block_height: u64, pub transaction_index: u32,
// Execution results pub status: ExecutionStatus, pub gas_used: u64, pub gas_refund: u64,
// State changes pub state_root: Hash, pub logs: Vec<Log>,
// Optional pub contract_address: Option<Address>, // If deployment}
#[derive(Debug, Clone, Serialize, Deserialize)]pub enum ExecutionStatus { Success, Reverted { reason: String }, OutOfGas,}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct Log { pub address: Address, pub topics: Vec<Hash>, // Indexed event parameters pub data: Vec<u8>, // Non-indexed data}Storing Receipts:
impl ChainStore { /// Store receipt indexed by transaction hash. pub fn put_receipt(&self, receipt: &TransactionReceipt) -> Result<()> { let key = format!("receipt:{}", receipt.transaction_hash.to_hex()); self.storage.put(key, receipt) }
/// Retrieve receipt by transaction hash. pub fn get_receipt(&self, tx_hash: &Hash) -> Result<Option<TransactionReceipt>> { let key = format!("receipt:{}", tx_hash.to_hex()); self.storage.get(key) }
/// Get all receipts for a block. pub fn get_receipts_for_block(&self, height: u64) -> Result<Vec<TransactionReceipt>> { let block = self.get_block_by_height(height)? .ok_or(StorageError::BlockNotFound)?; let mut receipts = Vec::new();
for tx in &block.body.transactions { let tx_hash = tx.hash(); if let Some(receipt) = self.get_receipt(&tx_hash)? { receipts.push(receipt); } }
Ok(receipts) }}Creating Receipts During Execution:
impl Executor { pub fn execute_with_receipt(&mut self, tx: &Transaction) -> Result<TransactionReceipt> { let tx_hash = tx.hash(); let start_gas = tx.gas_limit;
// Execute transaction let result = self.execute_transaction(tx);
// Compute gas used let gas_used = start_gas - self.gas_remaining;
// Determine status let status = match result { Ok(_) => ExecutionStatus::Success, Err(VmError::OutOfGas) => ExecutionStatus::OutOfGas, Err(e) => ExecutionStatus::Reverted { reason: e.to_string(), }, };
// Build receipt let receipt = TransactionReceipt { transaction_hash: tx_hash, block_height: self.current_block_height, transaction_index: self.current_tx_index, status, gas_used, gas_refund: 0, // Gas refunds for storage deletion state_root: self.state.compute_state_root()?, logs: self.logs.drain(..).collect(), contract_address: if tx.is_deploy() { Some(tx.contract_address()?) } else { None }, };
Ok(receipt) }}Usage: Verify Transaction Execution
// User checks if their transaction succeededlet tx_hash = Hash::from_hex("0xABCD...")?;let receipt = chain.get_receipt(&tx_hash)?.expect("receipt not found");
match receipt.status { ExecutionStatus::Success => { println!("✓ Transaction succeeded"); println!(" Gas used: {}", receipt.gas_used); println!(" Block: {}", receipt.block_height); } ExecutionStatus::Reverted { reason } => { println!("✗ Transaction reverted: {}", reason); } ExecutionStatus::OutOfGas => { println!("✗ Transaction ran out of gas"); }}Event Logs in Receipts:
Contracts can emit logs (events) to notify external observers:
// In VM: LOG opcodepub fn op_log(&mut self, address_reg: u8, data_reg: u8, topic_count: u8) -> Result<()> { let address = self.context.contract_address; let data_ptr = self.registers[data_reg as usize]; let data_len = self.registers[(data_reg + 1) as usize]; let data = self.memory.read_range(data_ptr, data_len)?;
// Read topics from registers let mut topics = Vec::new(); for i in 0..topic_count { let topic_reg = data_reg + 2 + i; let topic_value = self.registers[topic_reg as usize]; topics.push(Hash::from_u64(topic_value)); }
// Create log entry let log = Log { address, topics, data, };
self.logs.push(log); Ok(())}Example: ERC-20 Transfer Event
// Event: Transfer(address indexed from, address indexed to, uint256 amount)
Log { address: token_contract_address, topics: [ keccak256("Transfer(address,address,uint256)"), // Event signature Hash::from(from_address), // Indexed: from Hash::from(to_address), // Indexed: to ], data: amount.to_le_bytes().to_vec(), // Non-indexed: amount}B.3 Light Clients & Merkle Proofs
Section titled “B.3 Light Clients & Merkle Proofs”A light client verifies blockchain state without downloading the full chain.
Full Node vs Light Client:
| Component | Full Node | Light Client |
|---|---|---|
| Storage | Full blocks + state | Only headers |
| Verification | Executes all transactions | Verifies Merkle proofs |
| Trust model | Trustless (re-executes) | Trusts PoA signatures |
| Bandwidth | ~100 MB/day | ~100 KB/day |
| Use case | Mining, archival | Mobile wallets, IoT |
Light Client Workflow:
Step 1: Sync Block Headers
pub struct LightClient { headers: Vec<BlockHeader>, trusted_authorities: HashSet<Address>,}
impl LightClient { pub fn sync_headers(&mut self, peer: &Peer) -> Result<()> { let latest_height = peer.get_chain_height()?; let our_height = self.headers.len() as u64;
// Download missing headers for h in (our_height + 1)..=latest_height { let header = peer.get_block_header(h)?;
// Verify authority signature self.verify_authority_signature(&header)?;
// Verify parent hash links correctly if h > 0 { let prev_hash = self.headers[(h - 1) as usize].hash(); if header.parent_hash != prev_hash { return Err(Error::InvalidChain); } }
self.headers.push(header); }
Ok(()) }
fn verify_authority_signature(&self, header: &BlockHeader) -> Result<()> { if !self.trusted_authorities.contains(&header.authority) { return Err(Error::UntrustedAuthority); }
let hash = header.signing_hash(); header.signature .as_ref() .ok_or(Error::MissingSignature)? .verify(&hash, &header.authority)?; Ok(()) }}Step 2: Request Account Proof
// User wants to check Alice's balance without downloading full statelet alice_addr = Address::from_hex("0xAA...")?;let proof = peer.get_account_proof(&alice_addr, block_height)?;Merkle Proof Structure:
#[derive(Debug, Clone)]pub struct AccountProof { pub address: Address, pub account: Account, pub proof: Vec<Hash>, // Merkle proof (sibling hashes) pub block_height: u64,}Step 3: Verify Proof Against State Root
impl LightClient { pub fn verify_account_proof(&self, proof: &AccountProof) -> Result<bool> { // Get trusted state root from header let header = &self.headers[proof.block_height as usize]; let trusted_state_root = header.state_root;
// Compute leaf hash (account data) let account_bytes = bincode::serialize(&proof.account)?; let leaf_hash = hash(&[proof.address.as_bytes(), &account_bytes].concat());
// Verify Merkle proof let computed_root = self.compute_merkle_root(leaf_hash, &proof.proof);
Ok(computed_root == trusted_state_root) }
fn compute_merkle_root(&self, leaf: Hash, proof: &[Hash]) -> Hash { let mut current = leaf;
for sibling in proof { // Combine hashes (order matters - left vs right) current = hash(&[current.as_bytes(), sibling.as_bytes()].concat()); }
current }}Visual Example: Merkle Proof for Account A
State Trie (4 accounts):
Root (0x7d3f...) / \ H(A,B) H(C,D) / \ / \ A(✓) B(proof) C(proof) D(proof)
Light client receives:- Account A data- Proof: [Hash(B), Hash(C,D)]
Verification:1. Compute H(A) from account data2. Combine H(A) + Hash(B) → H(A,B)3. Combine H(A,B) + Hash(C,D) → Root4. Compare with trusted state root from headerStep 4: Query Account Balance
// Complete light client querylet balance = light_client.get_balance_at(&alice_addr, block_height)?;
pub fn get_balance_at(&self, address: &Address, height: u64) -> Result<u64> { // Request proof from full node let proof = self.peer.get_account_proof(address, height)?;
// Verify proof against trusted header if !self.verify_account_proof(&proof)? { return Err(Error::InvalidProof); }
// Trust the account balance Ok(proof.account.balance)}Full Node: Generating Proofs
impl ChainStore { pub fn generate_account_proof( &self, address: &Address, height: u64, ) -> Result<AccountProof> { let state = self.state_at_height(height)?; let account = state.get_account(address)?;
// Collect all account hashes let mut accounts = self.get_all_accounts_at_height(height)?; accounts.sort_by_key(|a| a.address);
// Find target account index let target_idx = accounts.iter() .position(|a| a.address == *address) .ok_or(Error::AccountNotFound)?;
// Generate Merkle proof (sibling hashes on path to root) let proof = self.generate_merkle_proof(&accounts, target_idx)?;
Ok(AccountProof { address: *address, account, proof, block_height: height, }) }}Security Considerations:
Bandwidth Comparison:
| Operation | Full Node | Light Client |
|---|---|---|
| Sync block | ~10 KB (txs + state) | ~300 bytes (header only) |
| Verify balance | 0 (already has state) | ~500 bytes (proof) |
| Daily sync | ~100 MB | ~100 KB |
Real-World Example:
Ethereum light clients (Geth’s LES protocol) use this exact pattern:
- Download headers (~500 bytes each)
- Verify PoW/PoS consensus
- Request Merkle-Patricia proofs for specific accounts/storage
- Mobile wallets (MetaMask mobile, Status) use light clients
Summary
Section titled “Summary”These advanced topics extend Minichain’s core functionality to production-grade features:
| Concept | Purpose | Complexity | Use Case |
|---|---|---|---|
| Block Reorganization | Handle competing chains in multi-authority systems | High | Multi-authority PoA, PoS networks |
| Transaction Receipts | Provide verifiable execution results | Medium | DApp event tracking, execution verification |
| Light Clients | Enable trustless verification without full chain | High | Mobile wallets, IoT devices, resource-constrained nodes |
Key Takeaways
Section titled “Key Takeaways”-
Block Reorgs require sophisticated state management:
- Fork detection and common ancestor finding
- State reversion (snapshots or replay)
- Transaction mempool management
- Not needed in single-authority PoA
-
Transaction Receipts provide execution transparency:
- Indexed by transaction hash for fast lookup
- Include gas usage, status, and event logs
- Enable DApps to listen for contract events
- Separate from blocks for efficient storage
-
Light Clients democratize blockchain access:
- Download only headers (~300 bytes vs ~10 KB per block)
- Verify state with Merkle proofs
- Trust authority signatures (PoA/PoS)
- Enable mobile and resource-constrained devices
Production Implementation Notes
Section titled “Production Implementation Notes”When implementing these features for production:
- Reorgs: Use copy-on-write Merkle Patricia Tries for efficient state snapshots
- Receipts: Index by block height and transaction hash; consider bloom filters for log queries
- Light Clients: Implement checkpoint sync for faster initial synchronization; use fraud proofs for enhanced security
These patterns are used in Ethereum, Polygon, and other major blockchains to balance security, performance, and accessibility.
For more on core blockchain implementation, return to Chapter 5: Blockchain Orchestration & Consensus.