Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1#!/usr/bin/env python32# Copyright (c) The Bitcoin Core developers3# Distributed under the MIT software license.5"""6Ensures that UTXO unserialization errors abort the node, and does not7cause a consensus divergence.9A valid UTXO is created and shared between two nodes. The raw database10entry is then deliberately modified on one node so it can no longer be11deserialized. When the other node spends that UTXO and mines a block,12the node with the unserializable entry must abort during block connection13rather than silently treating the coin as absent and marking the block14BLOCK_FAILED_VALID, which would cause it to permanently diverge from the15network's best chain.16"""18try:19import plyvel # type: ignore[import]20except ImportError:21plyvel = None23from test_framework.blocktools import COINBASE_MATURITY24from test_framework.test_framework import BitcoinTestFramework, SkipTest25from test_framework.util import assert_equal26from test_framework.wallet import MiniWallet29class UTXOAbortOnErrorTest(BitcoinTestFramework):30def set_test_params(self):31self.setup_clean_chain = True32self.num_nodes = 234def skip_test_if_missing_module(self):35if plyvel is None:36raise SkipTest("plyvel not available (pip install plyvel)")38def setup_network(self):39self.setup_nodes() # Start with nodes disconnected41def run_test(self):42node0, node1 = self.nodes44self.log.info("Mining mature coinbase on node0")45wallet0 = MiniWallet(node0)46self.generate(wallet0, COINBASE_MATURITY + 1, sync_fun=self.no_op)47assert_equal(node0.getblockcount(), COINBASE_MATURITY + 1)49# The coinbase of block 1 is now mature. This is the UTXO we will50# make unserializable on node0 and spend on node1.51coinbase_txid = node0.getblock(node0.getblockhash(1))['tx'][0]52assert node0.gettxout(coinbase_txid, 0) is not None, f"Expected UTXO {coinbase_txid}:0 to exist in UTXO set"54self.log.info("Preparing a spend of the mature coinbase UTXO (not broadcast)")55utxo = wallet0.get_utxo(txid=coinbase_txid, vout=0, mark_as_spent=False)56spend_tx_hex = wallet0.create_self_transfer(utxo_to_spend=utxo)['hex']58self.log.info("Sync node1 up to the tip, then isolate nodes")59self.connect_nodes(0, 1)60self.sync_blocks()61assert_equal(node1.getblockcount(), COINBASE_MATURITY + 1)62self.disconnect_nodes(0, 1)64self.log.info("Make UTXO unserializable in node0 database")65self.stop_node(0)67# LevelDB key for the CoinEntry serialization:68# key = DB_COIN (0x43='C') || txid (32 bytes) || VARINT(vout=0)69coin_key = b'\x43' + bytes.fromhex(coinbase_txid)[::-1] + b'\x00'70chainstate_path = str(node0.chain_path / "chainstate")72# Update entry to mimic an incompatible serialization format73with plyvel.DB(chainstate_path, create_if_missing=False, compression=None) as db:74existing_value = db.get(coin_key)75assert existing_value is not None, f"UTXO {coinbase_txid}:0 not found in db"77# Write a single-byte value. After XOR deobfuscation this is still just one78# byte, far too short for a valid Coin (which needs height VARINT + amount79# VARINT + script at minimum). Deserialization will throw "end of data" when80# trying to read beyond the first field.81db.put(coin_key, b'\x00')83self.log.info("Restart node0 — the unserializable entry is only visible during block validation, not at startup")84self.start_node(0)86# Individual coin values are not read at startup; only block validation87# touches them. The node starts cleanly regardless of the tampered entry.88assert_equal(node0.getblockcount(), COINBASE_MATURITY + 1)90# Now spend the corrupted UTXO91self.log.info("node1 broadcasts the spend and mines a block")92node1.sendrawtransaction(spend_tx_hex)93spending_block_hash = self.generate(node1, 1, sync_fun=self.no_op)[0]94assert_equal(node1.getblockcount(), COINBASE_MATURITY + 2)96self.log.info("Connect node0 to node1 — node0 must abort when it tries to connect the spending block")97# Verify that the unserializable entry triggers the expected error and is98# never silently misreported as a missing input (bad-txns-inputs-missingorspent),99# which would indicate the previous silent-divergence behaviour.100with node0.assert_debug_log(expected_msgs=["Error reading from database: Coin deserialization failure"],101unexpected_msgs=["bad-txns-inputs-missingorspent"]):102try:103self.connect_nodes(0, 1)104except Exception:105pass # node0 may validly abort before connect_nodes returns107# Confirm node0 aborted with SIGABRT108self.wait_until(lambda: self.nodes[0].is_node_stopped(109expected_ret_code=-6,110expected_stderr="Error: Error reading from database, shutting down.",111))113self.log.info("node0 aborted cleanly — no silent divergence occurred")115self.log.info("Restart node0 and verify the spending block was not permanently marked invalid")116self.start_node(0)117assert_equal(node0.getblockcount(), COINBASE_MATURITY + 1)119# If BLOCK_FAILED_VALID had been written to disk, the node would be120# permanently stuck on a stale tip even after the tampered entry is resolved.121tips = node0.getchaintips()122permanently_invalid = [123t for t in tips124if t['hash'] == spending_block_hash and t['status'] == 'invalid'125]126assert len(permanently_invalid) == 0, f"Spending block {spending_block_hash} must not be marked BLOCK_FAILED_VALID"129if __name__ == '__main__':130UTXOAbortOnErrorTest(__file__).main()
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.