mirror of https://github.com/bitcoin/bitcoin.git
Merge aeb22c4361
into b510893d00
This commit is contained in:
commit
abf86587ff
|
@ -3010,13 +3010,64 @@ class TemporaryRollback
|
||||||
{
|
{
|
||||||
ChainstateManager& m_chainman;
|
ChainstateManager& m_chainman;
|
||||||
const CBlockIndex& m_invalidate_index;
|
const CBlockIndex& m_invalidate_index;
|
||||||
|
std::vector<uint256> m_invalidated_fork_blocks;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
TemporaryRollback(ChainstateManager& chainman, const CBlockIndex& index) : m_chainman(chainman), m_invalidate_index(index) {
|
TemporaryRollback(ChainstateManager& chainman, const CBlockIndex& index) : m_chainman(chainman), m_invalidate_index(index) {
|
||||||
|
// First, invalidate any competing fork blocks to prevent reorg during main chain invalidation
|
||||||
|
InvalidateCompetingForks();
|
||||||
|
|
||||||
|
// Then invalidate the main chain block for rollback
|
||||||
InvalidateBlock(m_chainman, m_invalidate_index.GetBlockHash());
|
InvalidateBlock(m_chainman, m_invalidate_index.GetBlockHash());
|
||||||
};
|
};
|
||||||
|
|
||||||
~TemporaryRollback() {
|
~TemporaryRollback() {
|
||||||
|
// Restore main chain block first
|
||||||
ReconsiderBlock(m_chainman, m_invalidate_index.GetBlockHash());
|
ReconsiderBlock(m_chainman, m_invalidate_index.GetBlockHash());
|
||||||
|
|
||||||
|
// Then restore all fork blocks
|
||||||
|
for (const uint256& fork_hash : m_invalidated_fork_blocks) {
|
||||||
|
ReconsiderBlock(m_chainman, fork_hash);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
void InvalidateCompetingForks() {
|
||||||
|
LOCK(m_chainman.GetMutex());
|
||||||
|
|
||||||
|
// Find the target height (the height we want to roll back to)
|
||||||
|
const CBlockIndex* target_index = m_invalidate_index.pprev;
|
||||||
|
if (!target_index) return; // Genesis block case
|
||||||
|
|
||||||
|
const int target_height = target_index->nHeight;
|
||||||
|
|
||||||
|
// Iterate through all known block indices to find competing forks
|
||||||
|
for (const auto& [hash, block_index] : m_chainman.m_blockman.m_block_index) {
|
||||||
|
// Skip if this block is on the active chain
|
||||||
|
if (m_chainman.ActiveChain().Contains(&block_index)) continue;
|
||||||
|
|
||||||
|
// Skip if this block is at or below the target height
|
||||||
|
if (block_index.nHeight <= target_height) continue;
|
||||||
|
|
||||||
|
// Skip if this block doesn't have valid data
|
||||||
|
if (!(block_index.nStatus & BLOCK_HAVE_DATA)) continue;
|
||||||
|
|
||||||
|
// Check if this fork block could interfere with rollback
|
||||||
|
// by tracing back to see if it forks at or after the target height
|
||||||
|
const CBlockIndex* fork_ancestor = &block_index;
|
||||||
|
while (fork_ancestor && fork_ancestor->nHeight > target_height) {
|
||||||
|
fork_ancestor = fork_ancestor->pprev;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we can trace this fork back to the target height or below,
|
||||||
|
// and it's not on the active chain, it's a competing fork
|
||||||
|
if (fork_ancestor && fork_ancestor->nHeight <= target_height) {
|
||||||
|
// Invalidate this fork block to prevent reorg
|
||||||
|
InvalidateBlock(m_chainman, hash);
|
||||||
|
m_invalidated_fork_blocks.push_back(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2025 The Bitcoin Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
"""Test dumptxoutset rollback with competing forks.
|
||||||
|
|
||||||
|
Test that dumptxoutset rollback functionality works correctly when competing
|
||||||
|
forks exist at or after the target rollback height.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from test_framework.test_framework import BitcoinTestFramework
|
||||||
|
from test_framework.util import (
|
||||||
|
assert_equal,
|
||||||
|
assert_raises_rpc_error,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DumptxoutsetForksTest(BitcoinTestFramework):
|
||||||
|
def set_test_params(self):
|
||||||
|
self.setup_clean_chain = True
|
||||||
|
self.num_nodes = 2
|
||||||
|
|
||||||
|
def setup_network(self):
|
||||||
|
self.setup_nodes()
|
||||||
|
|
||||||
|
def create_common_chain(self):
|
||||||
|
"""Create a common blockchain that both nodes will share."""
|
||||||
|
self.log.info("Creating common blockchain to height 15")
|
||||||
|
self.connect_nodes(0, 1)
|
||||||
|
self.generate(self.nodes[0], 15, sync_fun=self.sync_all)
|
||||||
|
|
||||||
|
assert_equal(self.nodes[0].getblockcount(), 15)
|
||||||
|
assert_equal(self.nodes[1].getblockcount(), 15)
|
||||||
|
|
||||||
|
return 15, self.nodes[0].getblockhash(15)
|
||||||
|
|
||||||
|
def test_baseline_functionality(self, target_height, target_hash):
|
||||||
|
"""Test that dumptxoutset works before forks exist."""
|
||||||
|
self.log.info("Testing baseline dumptxoutset functionality")
|
||||||
|
baseline_result = self.nodes[0].dumptxoutset("baseline_utxo.dat", rollback=target_height)
|
||||||
|
assert_equal(baseline_result['base_height'], target_height)
|
||||||
|
assert_equal(baseline_result['base_hash'], target_hash)
|
||||||
|
|
||||||
|
def create_competing_forks(self):
|
||||||
|
"""Create competing forks by disconnecting nodes and mining separately."""
|
||||||
|
self.log.info("Disconnecting nodes and creating competing forks")
|
||||||
|
self.disconnect_nodes(0, 1)
|
||||||
|
|
||||||
|
self.generate(self.nodes[0], 3, sync_fun=lambda: None)
|
||||||
|
|
||||||
|
self.generate(self.nodes[1], 2, sync_fun=lambda: None)
|
||||||
|
|
||||||
|
assert_equal(self.nodes[0].getblockcount(), 18)
|
||||||
|
assert_equal(self.nodes[1].getblockcount(), 17)
|
||||||
|
|
||||||
|
def transfer_fork_to_main_node(self):
|
||||||
|
"""Make the main node aware of the competing fork."""
|
||||||
|
self.log.info("Transferring competing fork blocks to main node")
|
||||||
|
for height in range(16, 18):
|
||||||
|
fork_hash = self.nodes[1].getblockhash(height)
|
||||||
|
fork_data = self.nodes[1].getblock(fork_hash, 0)
|
||||||
|
self.nodes[0].submitblock(fork_data)
|
||||||
|
|
||||||
|
def verify_fork_visibility(self):
|
||||||
|
"""Verify that the main node can see both chains."""
|
||||||
|
self.log.info("Verifying fork visibility")
|
||||||
|
chaintips = self.nodes[0].getchaintips()
|
||||||
|
assert len(chaintips) >= 2, f"Expected at least 2 chain tips, got {len(chaintips)}"
|
||||||
|
|
||||||
|
active_tip = [tip for tip in chaintips if tip['status'] == 'active'][0]
|
||||||
|
fork_tips = [tip for tip in chaintips if tip.get('branchlen', 0) > 0]
|
||||||
|
assert len(fork_tips) >= 1, "Expected at least one competing fork"
|
||||||
|
|
||||||
|
assert_equal(active_tip['height'], 18)
|
||||||
|
return active_tip, fork_tips
|
||||||
|
|
||||||
|
def test_rollback_with_forks(self, target_height, target_hash):
|
||||||
|
"""Test that dumptxoutset rollback works correctly even when competing forks are present."""
|
||||||
|
self.log.info("Testing dumptxoutset rollback with competing forks present")
|
||||||
|
|
||||||
|
original_tip = self.nodes[0].getbestblockhash()
|
||||||
|
original_height = self.nodes[0].getblockcount()
|
||||||
|
|
||||||
|
# This should now work correctly with our fix
|
||||||
|
result = self.nodes[0].dumptxoutset("fork_test_utxo.dat", rollback=target_height)
|
||||||
|
|
||||||
|
# Verify the snapshot was created from the correct block on the main chain
|
||||||
|
assert_equal(result['base_height'], target_height)
|
||||||
|
assert_equal(result['base_hash'], target_hash)
|
||||||
|
|
||||||
|
# Verify node state is restored after successful rollback
|
||||||
|
current_tip = self.nodes[0].getbestblockhash()
|
||||||
|
current_height = self.nodes[0].getblockcount()
|
||||||
|
assert_equal(current_tip, original_tip)
|
||||||
|
assert_equal(current_height, original_height)
|
||||||
|
|
||||||
|
def run_test(self):
|
||||||
|
"""Main test logic"""
|
||||||
|
mocktime = self.nodes[0].getblockheader(self.nodes[0].getblockhash(0))['time'] + 1
|
||||||
|
for node in self.nodes:
|
||||||
|
node.setmocktime(mocktime)
|
||||||
|
|
||||||
|
target_height, target_hash = self.create_common_chain()
|
||||||
|
self.test_baseline_functionality(target_height, target_hash)
|
||||||
|
|
||||||
|
self.create_competing_forks()
|
||||||
|
self.transfer_fork_to_main_node()
|
||||||
|
self.verify_fork_visibility()
|
||||||
|
|
||||||
|
# Test the main functionality
|
||||||
|
self.test_rollback_with_forks(target_height, target_hash)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
DumptxoutsetForksTest(__file__).main()
|
|
@ -299,6 +299,7 @@ BASE_SCRIPTS = [
|
||||||
'wallet_resendwallettransactions.py',
|
'wallet_resendwallettransactions.py',
|
||||||
'wallet_fallbackfee.py',
|
'wallet_fallbackfee.py',
|
||||||
'rpc_dumptxoutset.py',
|
'rpc_dumptxoutset.py',
|
||||||
|
'rpc_dumptxoutset_forks.py',
|
||||||
'feature_minchainwork.py',
|
'feature_minchainwork.py',
|
||||||
'rpc_estimatefee.py',
|
'rpc_estimatefee.py',
|
||||||
'rpc_getblockstats.py',
|
'rpc_getblockstats.py',
|
||||||
|
|
Loading…
Reference in New Issue