TeachMeBitcoin

Building a UTXO & Mempool Reorg Simulator in Python

From TeachMeBitcoin, the free encyclopedia Reading time: 3 min

Building a UTXO & Mempool Reorg Simulator in Python

To understand how a node's internal state changes during a reorganization, we can build a simulator that tracks a UTXO Database and a Mempool. This script demonstrates how transactions are "undone" and moved back to the mempool when a block is disconnected.


1. The UTXO Reorg Simulator Source Code

Save the following Python script as utxo_reorg_sim.py.

class UTXOState:
 def __init__(self):
 # Format: { "txid:vout": "owner" }
 self.utxo_set = {
 "genesis:0": "Alice (50 BTC)"
 }
 self.mempool = []
 self.block_history = []

 def connect_block(self, block):
 print(f"\n[+] Connecting Block {block['height']} ({block['id']})")

 # 1. Remove consumed inputs from UTXO set
 for tx in block['txs']:
 if tx['id'] == "coinbase": continue # Skip coinbase input check

 input_key = f"{tx['input_id']}:{tx['input_vout']}"
 if input_key in self.utxo_set:
 del self.utxo_set[input_key]
 print(f" ├─ Consumed Input: {input_key}")

 # Remove from mempool if present
 self.mempool = [m for m in self.mempool if m['id'] != tx['id']]

 # 2. Add new outputs to UTXO set
 for tx in block['txs']:
 for i, out in enumerate(tx['outputs']):
 output_key = f"{tx['id']}:{i}"
 self.utxo_set[output_key] = f"{out['owner']} ({out['amount']} BTC)"
 print(f" ├─ Created UTXO: {output_key}")

 self.block_history.append(block)

 def disconnect_block(self):
 if not self.block_history: return
 block = self.block_history.pop()

 print(f"\n[-] Disconnecting Block {block['height']} ({block['id']})")

 # 1. Remove outputs created by this block
 for tx in block['txs']:
 for i in range(len(tx['outputs'])):
 output_key = f"{tx['id']}:{i}"
 if output_key in self.utxo_set:
 del self.utxo_set[output_key]
 print(f" ├─ Removed UTXO: {output_key}")

 # 2. Restore inputs consumed by this block
 for tx in block['txs']:
 if tx['id'] == "coinbase": continue
 input_key = f"{tx['input_id']}:{tx['input_vout']}"
 # In a real node, we'd look up the original owner/amount in undo data
 self.utxo_set[input_key] = "Restored from Undo Data"
 print(f" ├─ Restored Input: {input_key}")

 # 3. Move transactions back to mempool
 self.mempool.append(tx)
 print(f" ├─ Moved TX {tx['id']} back to Mempool")

 def show_state(self):
 print("\n=== CURRENT NODE STATE ===")
 print(f"[*] UTXO Set: {list(self.utxo_set.keys())}")
 print(f"[*] Mempool: {[m['id'] for m in self.mempool]}")

if __name__ == "__main__":
 node = UTXOState()

 # Block 1: Alice sends to Bob
 block1 = {
 "height": 1, "id": "B1",
 "txs": [
 {"id": "tx1", "input_id": "genesis", "input_vout": 0, "outputs": [{"owner": "Bob", "amount": 10}]}
 ]
 }

 # Block 2A: Bob sends to Charlie
 block2a = {
 "height": 2, "id": "B2A",
 "txs": [
 {"id": "tx2", "input_id": "tx1", "input_vout": 0, "outputs": [{"owner": "Charlie", "amount": 9}]}
 ]
 }

 node.connect_block(block1)
 node.connect_block(block2a)
 node.show_state()

 # --- TRIGGER REORG ---
 # Node finds out Block 2A is invalid or on a shorter chain
 print("\n" + "!"*30 + "\n! TRIGGERING REORGANIZATION !\n" + "!"*30)
 node.disconnect_block()
 node.show_state()

️ 2. Key Takeaways

  1. State Reversal: Notice how tx2 is completely wiped from the UTXO set and Bob's input (tx1:0) is magically restored.

  2. Mempool Fluidity: The transaction tx2 isn't lost. It sits in the mempool, waiting for the node to find a new winning Block 2 (or 3) that can include it.

  3. Consistency: This process ensures that no matter how many forks happen, every node that reaches the same block height on the same chain will have an identical UTXO database.

☕ Help support TeachMeBitcoin

TeachMeBitcoin is an ad-free, open-source educational repository curated by a passionate team of Bitcoin researchers and educators for public benefit. If you found our articles helpful, please consider supporting our hosting and ongoing content updates with a clean donation:

Ethereum: 0x578417C51783663D8A6A811B3544E1f779D39A85
Bitcoin: bc1q77k9e95rn669kpzyjr8ke9w95zhk7pa5s63qzz
Solana: 4ycT2ayqeMucixj3wS8Ay8Tq9NRDYRPKYbj3UGESyQ4J
Address copied to clipboard!