Skip to content

Commit 0cac457

Browse files
committed
Merge bitcoin/bitcoin#30320: assumeutxo: Don't load a snapshot if it's not in the best header chain
55b6d7b validation: Don't load a snapshot if it's not in the best header chain. (Martin Zumsande) Pull request description: This was suggested by me in the discussion of #30288, which has more context. If the snapshot is not an ancestor of the most-work header (`m_best_header`), syncing from that alternative chain leading to `m_best_header` should be prioritised. Therefore it's not useful loading the snapshot in this situation. If the other chain turns out to be invalid or the chain with the snapshot retrieves additional headers so that it's the most-work one again (see functional test), `m_best_header` will change and loading the snapshot will be possible again. Because of the work required to generate a conflicting headers chain, a situation with two conflicting chains should only be possible under extreme circumstances, such as major forks. ACKs for top commit: fjahr: re-ACK 55b6d7b achow101: ACK 55b6d7b alfonsoromanz: Re ACK 55b6d7b Tree-SHA512: 4fbea5ab1038ae353fc949a186041cf9b397e7ce4ac59ff36f881c9437b4f22ada922490ead5b2661389eb1ca0f3d1e7e7e6a4261057678643e71594a691ac36
2 parents 6144aa2 + 55b6d7b commit 0cac457

File tree

2 files changed

+33
-3
lines changed

2 files changed

+33
-3
lines changed

src/validation.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5668,6 +5668,10 @@ util::Result<void> ChainstateManager::ActivateSnapshot(
56685668
return util::Error{strprintf(Untranslated("The base block header (%s) is part of an invalid chain"), base_blockhash.ToString())};
56695669
}
56705670

5671+
if (!m_best_header || m_best_header->GetAncestor(base_blockheight) != snapshot_start_block) {
5672+
return util::Error{_("A forked headers-chain with more work than the chain with the snapshot base block header exists. Please proceed to sync without AssumeUtxo.")};
5673+
}
5674+
56715675
auto mempool{m_active_chainstate->GetMempool()};
56725676
if (mempool && mempool->size() > 0) {
56735677
return util::Error{Untranslated("Can't activate a snapshot when mempool not empty")};

test/functional/feature_assumeutxo.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,21 @@
1515
1616
- TODO: Valid snapshot file, but referencing a snapshot block that turns out to be
1717
invalid, or has an invalid parent
18-
- TODO: Valid snapshot file and snapshot block, but the block is not on the
19-
most-work chain
2018
2119
Interesting starting states could be loading a snapshot when the current chain tip is:
2220
2321
- TODO: An ancestor of snapshot block
2422
- TODO: The snapshot block
2523
- TODO: A descendant of the snapshot block
26-
- TODO: Not an ancestor or a descendant of the snapshot block and has more work
2724
2825
"""
2926
from shutil import rmtree
3027

3128
from dataclasses import dataclass
29+
from test_framework.blocktools import (
30+
create_block,
31+
create_coinbase
32+
)
3233
from test_framework.messages import tx_from_hex
3334
from test_framework.test_framework import BitcoinTestFramework
3435
from test_framework.util import (
@@ -241,6 +242,30 @@ def test_snapshot_in_a_divergent_chain(self, dump_output_path):
241242
self.sync_blocks(nodes=(n0, n3))
242243
self.wait_until(lambda: len(n3.getchainstates()['chainstates']) == 1)
243244

245+
def test_snapshot_not_on_most_work_chain(self, dump_output_path):
246+
self.log.info("Test snapshot is not loaded when the node knows the headers of another chain with more work.")
247+
node0 = self.nodes[0]
248+
node1 = self.nodes[1]
249+
# Create an alternative chain of 2 new blocks, forking off the main chain at the block before the snapshot block.
250+
# This simulates a longer chain than the main chain when submitting these two block headers to node 1 because it is only aware of
251+
# the main chain headers up to the snapshot height.
252+
parent_block_hash = node0.getblockhash(SNAPSHOT_BASE_HEIGHT - 1)
253+
block_time = node0.getblock(node0.getbestblockhash())['time'] + 1
254+
fork_block1 = create_block(int(parent_block_hash, 16), create_coinbase(SNAPSHOT_BASE_HEIGHT), block_time)
255+
fork_block1.solve()
256+
fork_block2 = create_block(fork_block1.sha256, create_coinbase(SNAPSHOT_BASE_HEIGHT + 1), block_time + 1)
257+
fork_block2.solve()
258+
node1.submitheader(fork_block1.serialize().hex())
259+
node1.submitheader(fork_block2.serialize().hex())
260+
msg = "A forked headers-chain with more work than the chain with the snapshot base block header exists. Please proceed to sync without AssumeUtxo."
261+
assert_raises_rpc_error(-32603, msg, node1.loadtxoutset, dump_output_path)
262+
# Cleanup: submit two more headers of the snapshot chain to node 1, so that it is the most-work chain again and loading
263+
# the snapshot in future subtests succeeds
264+
main_block1 = node0.getblock(node0.getblockhash(SNAPSHOT_BASE_HEIGHT + 1), 0)
265+
main_block2 = node0.getblock(node0.getblockhash(SNAPSHOT_BASE_HEIGHT + 2), 0)
266+
node1.submitheader(main_block1)
267+
node1.submitheader(main_block2)
268+
244269
def run_test(self):
245270
"""
246271
Bring up two (disconnected) nodes, mine some new blocks on the first,
@@ -330,6 +355,7 @@ def run_test(self):
330355
self.test_invalid_chainstate_scenarios()
331356
self.test_invalid_file_path()
332357
self.test_snapshot_block_invalidated(dump_output['path'])
358+
self.test_snapshot_not_on_most_work_chain(dump_output['path'])
333359

334360
self.log.info(f"Loading snapshot into second node from {dump_output['path']}")
335361
loaded = n1.loadtxoutset(dump_output['path'])

0 commit comments

Comments
 (0)