rpc: Fix dumptxoutset rollback with competing forks

When dumptxoutset rollback is used and competing forks exist at the target
height, the InvalidateBlock call would cause a reorg to the competing fork
instead of rolling back to the desired block on the main chain.

Fix by invalidating all competing fork blocks first before invalidating
the main chain block. This prevents reorgs during rollback and ensures
the snapshot is created from the correct main chain block.

The fix enhances the TemporaryRollback class to:
- Identify competing fork blocks at heights above the target
- Invalidate all fork blocks before main chain invalidation
- Restore all invalidated blocks in the destructor
This commit is contained in:
Ehnamuram Enoch 2025-09-20 23:05:50 +01:00
parent d615fbfa93
commit aeb22c4361
2 changed files with 61 additions and 10 deletions

View File

@ -3001,13 +3001,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);
}
}
}
}; };
/** /**

View File

@ -74,21 +74,21 @@ class DumptxoutsetForksTest(BitcoinTestFramework):
assert_equal(active_tip['height'], 18) assert_equal(active_tip['height'], 18)
return active_tip, fork_tips return active_tip, fork_tips
def test_rollback_with_forks(self, target_height): def test_rollback_with_forks(self, target_height, target_hash):
"""Test that dumptxoutset rollback fails when competing forks are present.""" """Test that dumptxoutset rollback works correctly even when competing forks are present."""
self.log.info("Testing dumptxoutset rollback with competing forks present") self.log.info("Testing dumptxoutset rollback with competing forks present")
original_tip = self.nodes[0].getbestblockhash() original_tip = self.nodes[0].getbestblockhash()
original_height = self.nodes[0].getblockcount() original_height = self.nodes[0].getblockcount()
assert_raises_rpc_error( # This should now work correctly with our fix
-1, result = self.nodes[0].dumptxoutset("fork_test_utxo.dat", rollback=target_height)
"Could not roll back to requested height",
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_tip = self.nodes[0].getbestblockhash()
current_height = self.nodes[0].getblockcount() current_height = self.nodes[0].getblockcount()
assert_equal(current_tip, original_tip) assert_equal(current_tip, original_tip)
@ -108,7 +108,7 @@ class DumptxoutsetForksTest(BitcoinTestFramework):
self.verify_fork_visibility() self.verify_fork_visibility()
# Test the main functionality # Test the main functionality
self.test_rollback_with_forks(target_height) self.test_rollback_with_forks(target_height, target_hash)
if __name__ == '__main__': if __name__ == '__main__':