Skip to content

Appendix C: Advanced Blockchain Concepts

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.


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: Genesis
T=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 fork
for block in new_chain_blocks {
self.execute_block(&block)?;
self.put_block(&block)?;
}

Step 5: Update Mempool

// Re-add reverted transactions
for 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 → 4C

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 succeeded
let 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 opcode
pub 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
}

A light client verifies blockchain state without downloading the full chain.

Full Node vs Light Client:

ComponentFull NodeLight Client
StorageFull blocks + stateOnly headers
VerificationExecutes all transactionsVerifies Merkle proofs
Trust modelTrustless (re-executes)Trusts PoA signatures
Bandwidth~100 MB/day~100 KB/day
Use caseMining, archivalMobile 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 state
let 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 data
2. Combine H(A) + Hash(B) → H(A,B)
3. Combine H(A,B) + Hash(C,D) → Root
4. Compare with trusted state root from header

Step 4: Query Account Balance

// Complete light client query
let 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:

OperationFull NodeLight Client
Sync block~10 KB (txs + state)~300 bytes (header only)
Verify balance0 (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:

  1. Download headers (~500 bytes each)
  2. Verify PoW/PoS consensus
  3. Request Merkle-Patricia proofs for specific accounts/storage
  4. Mobile wallets (MetaMask mobile, Status) use light clients

These advanced topics extend Minichain’s core functionality to production-grade features:

ConceptPurposeComplexityUse Case
Block ReorganizationHandle competing chains in multi-authority systemsHighMulti-authority PoA, PoS networks
Transaction ReceiptsProvide verifiable execution resultsMediumDApp event tracking, execution verification
Light ClientsEnable trustless verification without full chainHighMobile wallets, IoT devices, resource-constrained nodes
  1. 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
  2. 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
  3. 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

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.