Skip to content

Commit ab163b0

Browse files
committed
Merge bitcoin/bitcoin#27823: init: return error when block index is non-contiguous, fix feature_init.py file perturbation
d27b9a2 test: fix feature_init.py file perturbation (Martin Zumsande) ad66ca1 init: abort loading of blockindex in case of missing height. (Martin Zumsande) Pull request description: When the block index database is non-contiguous due to file corruption (i.e. it contains indexes of height `x-1` and `x+1`, but not `x`), bitcoind can currently crash with an assert in `BuildSkip()` / `GetAncestor()` during `BlockManager::LoadBlockIndex()`: ``` bitcoind: chain.cpp:112: const CBlockIndex* CBlockIndex::GetAncestor(int) const: Assertion `pindexWalk->pprev' failed. ``` This PR changes it such that we instead return an `InitError` to the user. I stumbled upon this because I noticed that the file perturbation in `feature_init.py` wasn't working as intended, which is fixed in the second commit: * Opening the file twice in one `with` statement would lead to `tf_read` being empty, so the test wouldn't perturb anything but replace the file with a new one. Fixed by first opening for read, then for write. * We need to restore the previous state after perturbations, so that only the current perturbation is active and not a mix of the current and previous ones. * I also added `checkblocks=200` to the startup parameters so that corruption in earlier blocks of `blk00000.dat` is detected during init verification and not ignored. After fixing `feature_init.py` like that I'd run into the `assert` mentioned above (so running the testfix from the second commit without the first one is a way to reproduce it). ACKs for top commit: achow101: ACK d27b9a2 furszy: Code ACK d27b9a2 fjahr: Code review ACK d27b9a2 Tree-SHA512: 2e54da6030c5813c86bd58f816401e090bb43c5b834764a5e3c0e55dbfe09e423f88042cab823db3742088204b274d4ad2abf58a3832a4b18328b11a30bf7094
2 parents 30b3477 + d27b9a2 commit ab163b0

File tree

2 files changed

+24
-5
lines changed

2 files changed

+24
-5
lines changed

src/node/blockstorage.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,8 +410,13 @@ bool BlockManager::LoadBlockIndex(const std::optional<uint256>& snapshot_blockha
410410
std::sort(vSortedByHeight.begin(), vSortedByHeight.end(),
411411
CBlockIndexHeightOnlyComparator());
412412

413+
CBlockIndex* previous_index{nullptr};
413414
for (CBlockIndex* pindex : vSortedByHeight) {
414415
if (m_interrupt) return false;
416+
if (previous_index && pindex->nHeight > previous_index->nHeight + 1) {
417+
return error("%s: block index is non-contiguous, index of height %d missing", __func__, previous_index->nHeight + 1);
418+
}
419+
previous_index = pindex;
415420
pindex->nChainWork = (pindex->pprev ? pindex->pprev->nChainWork : 0) + GetBlockProof(*pindex);
416421
pindex->nTimeMax = (pindex->pprev ? std::max(pindex->pprev->nTimeMax, pindex->nTime) : pindex->nTime);
417422

test/functional/feature_init.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""Stress tests related to node initialization."""
66
import os
77
from pathlib import Path
8+
import shutil
89

910
from test_framework.test_framework import BitcoinTestFramework, SkipTest
1011
from test_framework.test_node import ErrorMatch
@@ -47,7 +48,7 @@ def sigterm_node():
4748

4849
def start_expecting_error(err_fragment):
4950
node.assert_start_raises_init_error(
50-
extra_args=['-txindex=1', '-blockfilterindex=1', '-coinstatsindex=1'],
51+
extra_args=['-txindex=1', '-blockfilterindex=1', '-coinstatsindex=1', '-checkblocks=200', '-checklevel=4'],
5152
expected_msg=err_fragment,
5253
match=ErrorMatch.PARTIAL_REGEX,
5354
)
@@ -101,9 +102,9 @@ def check_clean_start():
101102
}
102103

103104
files_to_perturb = {
104-
'blocks/index/*.ldb': 'Error opening block database.',
105+
'blocks/index/*.ldb': 'Error loading block database.',
105106
'chainstate/*.ldb': 'Error opening block database.',
106-
'blocks/blk*.dat': 'Error opening block database.',
107+
'blocks/blk*.dat': 'Corrupted block database detected.',
107108
}
108109

109110
for file_patt, err_fragment in files_to_delete.items():
@@ -124,18 +125,31 @@ def check_clean_start():
124125
check_clean_start()
125126
self.stop_node(0)
126127

128+
self.log.info("Test startup errors after perturbing certain essential files")
127129
for file_patt, err_fragment in files_to_perturb.items():
130+
shutil.copytree(node.chain_path / "blocks", node.chain_path / "blocks_bak")
131+
shutil.copytree(node.chain_path / "chainstate", node.chain_path / "chainstate_bak")
128132
target_files = list(node.chain_path.glob(file_patt))
129133

130134
for target_file in target_files:
131135
self.log.info(f"Perturbing file to ensure failure {target_file}")
132-
with open(target_file, "rb") as tf_read, open(target_file, "wb") as tf_write:
136+
with open(target_file, "rb") as tf_read:
133137
contents = tf_read.read()
134138
tweaked_contents = bytearray(contents)
135-
tweaked_contents[50:250] = b'1' * 200
139+
# Since the genesis block is not checked by -checkblocks, the
140+
# perturbation window must be chosen such that a higher block
141+
# in blk*.dat is affected.
142+
tweaked_contents[150:350] = b'1' * 200
143+
with open(target_file, "wb") as tf_write:
136144
tf_write.write(bytes(tweaked_contents))
137145

138146
start_expecting_error(err_fragment)
139147

148+
shutil.rmtree(node.chain_path / "blocks")
149+
shutil.rmtree(node.chain_path / "chainstate")
150+
shutil.move(node.chain_path / "blocks_bak", node.chain_path / "blocks")
151+
shutil.move(node.chain_path / "chainstate_bak", node.chain_path / "chainstate")
152+
153+
140154
if __name__ == '__main__':
141155
InitStressTest().main()

0 commit comments

Comments
 (0)