aboutsummaryrefslogtreecommitdiff
path: root/test/functional/feature_assumeutxo.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/functional/feature_assumeutxo.py')
-rwxr-xr-xtest/functional/feature_assumeutxo.py107
1 files changed, 91 insertions, 16 deletions
diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py
index 60dd751ff8..3e882f47b8 100755
--- a/test/functional/feature_assumeutxo.py
+++ b/test/functional/feature_assumeutxo.py
@@ -34,6 +34,7 @@ Interesting starting states could be loading a snapshot when the current chain t
"""
from shutil import rmtree
+from dataclasses import dataclass
from test_framework.messages import tx_from_hex
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
@@ -113,6 +114,12 @@ class AssumeutxoTest(BitcoinTestFramework):
f.write(valid_snapshot_contents[(32 + 8 + offset + len(content)):])
expected_error(log_msg=f"[snapshot] bad snapshot content hash: expected a4bf3407ccb2cc0145c49ebba8fa91199f8a3903daf0883875941497d2493c27, got {wrong_hash}")
+ def test_headers_not_synced(self, valid_snapshot_path):
+ for node in self.nodes[1:]:
+ assert_raises_rpc_error(-32603, "The base block header (3bb7ce5eba0be48939b7a521ac1ba9316afee2c7bada3a0cca24188e6d7d96c0) must appear in the headers chain. Make sure all headers are syncing, and call this RPC again.",
+ node.loadtxoutset,
+ valid_snapshot_path)
+
def test_invalid_chainstate_scenarios(self):
self.log.info("Test different scenarios of invalid snapshot chainstate in datadir")
@@ -127,7 +134,7 @@ class AssumeutxoTest(BitcoinTestFramework):
with self.nodes[0].assert_debug_log([log_msg]):
self.nodes[0].assert_start_raises_init_error(expected_msg=error_msg)
- expected_error_msg = f"Error: A fatal internal error occurred, see debug.log for details"
+ expected_error_msg = f"Error: A fatal internal error occurred, see debug.log for details: Assumeutxo data not found for the given blockhash '7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a'."
error_details = f"Assumeutxo data not found for the given blockhash"
expected_error(log_msg=error_details, error_msg=expected_error_msg)
@@ -166,26 +173,28 @@ class AssumeutxoTest(BitcoinTestFramework):
for n in self.nodes:
n.setmocktime(n.getblockheader(n.getbestblockhash())['time'])
- self.sync_blocks()
-
# Generate a series of blocks that `n0` will have in the snapshot,
- # but that n1 doesn't yet see. In order for the snapshot to activate,
- # though, we have to ferry over the new headers to n1 so that it
- # isn't waiting forever to see the header of the snapshot's base block
- # while disconnected from n0.
+ # but that n1 and n2 don't yet see.
+ assert n0.getblockcount() == START_HEIGHT
+ blocks = {START_HEIGHT: Block(n0.getbestblockhash(), 1, START_HEIGHT + 1)}
for i in range(100):
+ block_tx = 1
if i % 3 == 0:
self.mini_wallet.send_self_transfer(from_node=n0)
+ block_tx += 1
self.generate(n0, nblocks=1, sync_fun=self.no_op)
- newblock = n0.getblock(n0.getbestblockhash(), 0)
+ height = n0.getblockcount()
+ hash = n0.getbestblockhash()
+ blocks[height] = Block(hash, block_tx, blocks[height-1].chain_tx + block_tx)
+ if i == 4:
+ # Create a stale block that forks off the main chain before the snapshot.
+ temp_invalid = n0.getbestblockhash()
+ n0.invalidateblock(temp_invalid)
+ stale_hash = self.generateblock(n0, output="raw(aaaa)", transactions=[], sync_fun=self.no_op)["hash"]
+ n0.invalidateblock(stale_hash)
+ n0.reconsiderblock(temp_invalid)
+ stale_block = n0.getblock(stale_hash, 0)
- # make n1 aware of the new header, but don't give it the block.
- n1.submitheader(newblock)
- n2.submitheader(newblock)
-
- # Ensure everyone is seeing the same headers.
- for n in self.nodes:
- assert_equal(n.getblockchaininfo()["headers"], SNAPSHOT_BASE_HEIGHT)
self.log.info("-- Testing assumeutxo + some indexes + pruning")
@@ -195,10 +204,27 @@ class AssumeutxoTest(BitcoinTestFramework):
self.log.info(f"Creating a UTXO snapshot at height {SNAPSHOT_BASE_HEIGHT}")
dump_output = n0.dumptxoutset('utxos.dat')
+ self.log.info("Test loading snapshot when headers are not synced")
+ self.test_headers_not_synced(dump_output['path'])
+
+ # In order for the snapshot to activate, we have to ferry over the new
+ # headers to n1 and n2 so that they see the header of the snapshot's
+ # base block while disconnected from n0.
+ for i in range(1, 300):
+ block = n0.getblock(n0.getblockhash(i), 0)
+ # make n1 and n2 aware of the new header, but don't give them the
+ # block.
+ n1.submitheader(block)
+ n2.submitheader(block)
+
+ # Ensure everyone is seeing the same headers.
+ for n in self.nodes:
+ assert_equal(n.getblockchaininfo()["headers"], SNAPSHOT_BASE_HEIGHT)
+
assert_equal(
dump_output['txoutset_hash'],
"a4bf3407ccb2cc0145c49ebba8fa91199f8a3903daf0883875941497d2493c27")
- assert_equal(dump_output["nchaintx"], 334)
+ assert_equal(dump_output["nchaintx"], blocks[SNAPSHOT_BASE_HEIGHT].chain_tx)
assert_equal(n0.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT)
# Mine more blocks on top of the snapshot that n1 hasn't yet seen. This
@@ -219,6 +245,29 @@ class AssumeutxoTest(BitcoinTestFramework):
assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT)
assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT)
+ def check_tx_counts(final: bool) -> None:
+ """Check nTx and nChainTx intermediate values right after loading
+ the snapshot, and final values after the snapshot is validated."""
+ for height, block in blocks.items():
+ tx = n1.getblockheader(block.hash)["nTx"]
+ chain_tx = n1.getchaintxstats(nblocks=1, blockhash=block.hash)["txcount"]
+
+ # Intermediate nTx of the starting block should be set, but nTx of
+ # later blocks should be 0 before they are downloaded.
+ if final or height == START_HEIGHT:
+ assert_equal(tx, block.tx)
+ else:
+ assert_equal(tx, 0)
+
+ # Intermediate nChainTx of the starting block and snapshot block
+ # should be set, but others should be 0 until they are downloaded.
+ if final or height in (START_HEIGHT, SNAPSHOT_BASE_HEIGHT):
+ assert_equal(chain_tx, block.chain_tx)
+ else:
+ assert_equal(chain_tx, 0)
+
+ check_tx_counts(final=False)
+
normal, snapshot = n1.getchainstates()["chainstates"]
assert_equal(normal['blocks'], START_HEIGHT)
assert_equal(normal.get('snapshot_blockhash'), None)
@@ -229,6 +278,15 @@ class AssumeutxoTest(BitcoinTestFramework):
assert_equal(n1.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT)
+ self.log.info("Submit a stale block that forked off the chain before the snapshot")
+ # Normally a block like this would not be downloaded, but if it is
+ # submitted early before the background chain catches up to the fork
+ # point, it winds up in m_blocks_unlinked and triggers a corner case
+ # that previously crashed CheckBlockIndex.
+ n1.submitblock(stale_block)
+ n1.getchaintips()
+ n1.getblock(stale_hash)
+
self.log.info("Submit a spending transaction for a snapshot chainstate coin to the mempool")
# spend the coinbase output of the first block that is not available on node1
spend_coin_blockhash = n1.getblockhash(START_HEIGHT + 1)
@@ -266,6 +324,16 @@ class AssumeutxoTest(BitcoinTestFramework):
self.log.info("Restarted node before snapshot validation completed, reloading...")
self.restart_node(1, extra_args=self.extra_args[1])
+
+ # Send snapshot block to n1 out of order. This makes the test less
+ # realistic because normally the snapshot block is one of the last
+ # blocks downloaded, but its useful to test because it triggers more
+ # corner cases in ReceivedBlockTransactions() and CheckBlockIndex()
+ # setting and testing nChainTx values, and it exposed previous bugs.
+ snapshot_hash = n0.getblockhash(SNAPSHOT_BASE_HEIGHT)
+ snapshot_block = n0.getblock(snapshot_hash, 0)
+ n1.submitblock(snapshot_block)
+
self.connect_nodes(0, 1)
self.log.info(f"Ensuring snapshot chain syncs to tip. ({FINAL_HEIGHT})")
@@ -282,6 +350,8 @@ class AssumeutxoTest(BitcoinTestFramework):
}
self.wait_until(lambda: n1.getindexinfo() == completed_idx_state)
+ self.log.info("Re-check nTx and nChainTx values")
+ check_tx_counts(final=True)
for i in (0, 1):
n = self.nodes[i]
@@ -356,6 +426,11 @@ class AssumeutxoTest(BitcoinTestFramework):
self.connect_nodes(0, 2)
self.wait_until(lambda: n2.getblockcount() == FINAL_HEIGHT)
+@dataclass
+class Block:
+ hash: str
+ tx: int
+ chain_tx: int
if __name__ == '__main__':
AssumeutxoTest().main()