Merge bitcoin/bitcoin#30320: assumeutxo: Don't load a snapshot if it's not in the best header chain

55b6d7be68 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 55b6d7be68
  achow101:
    ACK 55b6d7be68
  alfonsoromanz:
    Re ACK 55b6d7be68

Tree-SHA512: 4fbea5ab1038ae353fc949a186041cf9b397e7ce4ac59ff36f881c9437b4f22ada922490ead5b2661389eb1ca0f3d1e7e7e6a4261057678643e71594a691ac36
This commit is contained in:
Ava Chow 2024-07-18 17:28:22 -04:00
commit 0cac45755e
No known key found for this signature in database
GPG key ID: 17565732E08E5E41
2 changed files with 33 additions and 3 deletions

View file

@ -5668,6 +5668,10 @@ util::Result<void> ChainstateManager::ActivateSnapshot(
return util::Error{strprintf(Untranslated("The base block header (%s) is part of an invalid chain"), base_blockhash.ToString())};
}
if (!m_best_header || m_best_header->GetAncestor(base_blockheight) != snapshot_start_block) {
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.")};
}
auto mempool{m_active_chainstate->GetMempool()};
if (mempool && mempool->size() > 0) {
return util::Error{Untranslated("Can't activate a snapshot when mempool not empty")};

View file

@ -15,20 +15,21 @@ Interesting test cases could be loading an assumeutxo snapshot file with:
- TODO: Valid snapshot file, but referencing a snapshot block that turns out to be
invalid, or has an invalid parent
- TODO: Valid snapshot file and snapshot block, but the block is not on the
most-work chain
Interesting starting states could be loading a snapshot when the current chain tip is:
- TODO: An ancestor of snapshot block
- TODO: The snapshot block
- TODO: A descendant of the snapshot block
- TODO: Not an ancestor or a descendant of the snapshot block and has more work
"""
from shutil import rmtree
from dataclasses import dataclass
from test_framework.blocktools import (
create_block,
create_coinbase
)
from test_framework.messages import tx_from_hex
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
@ -241,6 +242,30 @@ class AssumeutxoTest(BitcoinTestFramework):
self.sync_blocks(nodes=(n0, n3))
self.wait_until(lambda: len(n3.getchainstates()['chainstates']) == 1)
def test_snapshot_not_on_most_work_chain(self, dump_output_path):
self.log.info("Test snapshot is not loaded when the node knows the headers of another chain with more work.")
node0 = self.nodes[0]
node1 = self.nodes[1]
# Create an alternative chain of 2 new blocks, forking off the main chain at the block before the snapshot block.
# This simulates a longer chain than the main chain when submitting these two block headers to node 1 because it is only aware of
# the main chain headers up to the snapshot height.
parent_block_hash = node0.getblockhash(SNAPSHOT_BASE_HEIGHT - 1)
block_time = node0.getblock(node0.getbestblockhash())['time'] + 1
fork_block1 = create_block(int(parent_block_hash, 16), create_coinbase(SNAPSHOT_BASE_HEIGHT), block_time)
fork_block1.solve()
fork_block2 = create_block(fork_block1.sha256, create_coinbase(SNAPSHOT_BASE_HEIGHT + 1), block_time + 1)
fork_block2.solve()
node1.submitheader(fork_block1.serialize().hex())
node1.submitheader(fork_block2.serialize().hex())
msg = "A forked headers-chain with more work than the chain with the snapshot base block header exists. Please proceed to sync without AssumeUtxo."
assert_raises_rpc_error(-32603, msg, node1.loadtxoutset, dump_output_path)
# Cleanup: submit two more headers of the snapshot chain to node 1, so that it is the most-work chain again and loading
# the snapshot in future subtests succeeds
main_block1 = node0.getblock(node0.getblockhash(SNAPSHOT_BASE_HEIGHT + 1), 0)
main_block2 = node0.getblock(node0.getblockhash(SNAPSHOT_BASE_HEIGHT + 2), 0)
node1.submitheader(main_block1)
node1.submitheader(main_block2)
def run_test(self):
"""
Bring up two (disconnected) nodes, mine some new blocks on the first,
@ -330,6 +355,7 @@ class AssumeutxoTest(BitcoinTestFramework):
self.test_invalid_chainstate_scenarios()
self.test_invalid_file_path()
self.test_snapshot_block_invalidated(dump_output['path'])
self.test_snapshot_not_on_most_work_chain(dump_output['path'])
self.log.info(f"Loading snapshot into second node from {dump_output['path']}")
loaded = n1.loadtxoutset(dump_output['path'])