Building a UTXO & Mempool Reorg Simulator in Python
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
-
State Reversal: Notice how
tx2is completely wiped from the UTXO set and Bob's input (tx1:0) is magically restored. -
Mempool Fluidity: The transaction
tx2isn't lost. It sits in the mempool, waiting for the node to find a new winning Block 2 (or 3) that can include it. -
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.
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: