Custom Python UTXO Reorg Simulator
From TeachMeBitcoin, the free encyclopedia
⏱️ 3 min read
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.
☕ 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