Skip to content

Commit 6a1aa51

Browse files
committed
rpc: check block index before reading block / undo data
This avoids low-level log errors that are supposed to only occur when there is an actual problem with the block on disk missing unexpectedly, but not in the case where the block and/or undo data are expected not to be there. It changes behavior such that in the first case (block index indicates data is available but retrieving it fails) an error is thrown. It also adjusts a functional tests that tried to simulate not having undo data (but having block data) by deleting the undo file. This situation should occur reality because block and undo data are pruned together. Instead, test this situation with a block that hasn't been connected.
1 parent 6cbf2e5 commit 6a1aa51

File tree

3 files changed

+41
-22
lines changed

3 files changed

+41
-22
lines changed

src/rpc/blockchain.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,10 @@ UniValue blockToJSON(BlockManager& blockman, const CBlock& block, const CBlockIn
201201
case TxVerbosity::SHOW_DETAILS_AND_PREVOUT:
202202
CBlockUndo blockUndo;
203203
const bool is_not_pruned{WITH_LOCK(::cs_main, return !blockman.IsBlockPruned(blockindex))};
204-
const bool have_undo{is_not_pruned && blockman.UndoReadFromDisk(blockUndo, blockindex)};
205-
204+
bool have_undo{is_not_pruned && WITH_LOCK(::cs_main, return blockindex.nStatus & BLOCK_HAVE_UNDO)};
205+
if (have_undo && !blockman.UndoReadFromDisk(blockUndo, blockindex)) {
206+
throw JSONRPCError(RPC_INTERNAL_ERROR, "Undo data expected but can't be read. This could be due to disk corruption or a conflict with a pruning event.");
207+
}
206208
for (size_t i = 0; i < block.vtx.size(); ++i) {
207209
const CTransactionRef& tx = block.vtx.at(i);
208210
// coinbase transaction (i.e. i == 0) doesn't have undo data

src/rpc/rawtransaction.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,11 +405,16 @@ static RPCHelpMan getrawtransaction()
405405
CBlockUndo blockUndo;
406406
CBlock block;
407407

408-
if (tx->IsCoinBase() || !blockindex || WITH_LOCK(::cs_main, return chainman.m_blockman.IsBlockPruned(*blockindex)) ||
409-
!(chainman.m_blockman.UndoReadFromDisk(blockUndo, *blockindex) && chainman.m_blockman.ReadBlockFromDisk(block, *blockindex))) {
408+
if (tx->IsCoinBase() || !blockindex || WITH_LOCK(::cs_main, return !(blockindex->nStatus & BLOCK_HAVE_MASK))) {
410409
TxToJSON(*tx, hash_block, result, chainman.ActiveChainstate());
411410
return result;
412411
}
412+
if (!chainman.m_blockman.UndoReadFromDisk(blockUndo, *blockindex)) {
413+
throw JSONRPCError(RPC_INTERNAL_ERROR, "Undo data expected but can't be read. This could be due to disk corruption or a conflict with a pruning event.");
414+
}
415+
if (!chainman.m_blockman.ReadBlockFromDisk(block, *blockindex)) {
416+
throw JSONRPCError(RPC_INTERNAL_ERROR, "Block data expected but can't be read. This could be due to disk corruption or a conflict with a pruning event.");
417+
}
413418

414419
CTxUndo* undoTX {nullptr};
415420
auto it = std::find_if(block.vtx.begin(), block.vtx.end(), [tx](CTransactionRef t){ return *t == *tx; });

test/functional/rpc_blockchain.py

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,16 @@
3232
TIME_GENESIS_BLOCK,
3333
create_block,
3434
create_coinbase,
35+
create_tx_with_script,
3536
)
3637
from test_framework.messages import (
3738
CBlockHeader,
39+
COIN,
3840
from_hex,
3941
msg_block,
4042
)
4143
from test_framework.p2p import P2PInterface
42-
from test_framework.script import hash256
44+
from test_framework.script import hash256, OP_TRUE
4345
from test_framework.test_framework import BitcoinTestFramework
4446
from test_framework.util import (
4547
assert_equal,
@@ -556,12 +558,12 @@ def assert_hexblock_hashes(verbosity):
556558
block = node.getblock(blockhash, verbosity)
557559
assert_equal(blockhash, hash256(bytes.fromhex(block[:160]))[::-1].hex())
558560

559-
def assert_fee_not_in_block(verbosity):
560-
block = node.getblock(blockhash, verbosity)
561+
def assert_fee_not_in_block(hash, verbosity):
562+
block = node.getblock(hash, verbosity)
561563
assert 'fee' not in block['tx'][1]
562564

563-
def assert_fee_in_block(verbosity):
564-
block = node.getblock(blockhash, verbosity)
565+
def assert_fee_in_block(hash, verbosity):
566+
block = node.getblock(hash, verbosity)
565567
tx = block['tx'][1]
566568
assert 'fee' in tx
567569
assert_equal(tx['fee'], tx['vsize'] * fee_per_byte)
@@ -580,8 +582,8 @@ def assert_vin_contains_prevout(verbosity):
580582
total_vout += vout["value"]
581583
assert_equal(total_vin, total_vout + tx["fee"])
582584

583-
def assert_vin_does_not_contain_prevout(verbosity):
584-
block = node.getblock(blockhash, verbosity)
585+
def assert_vin_does_not_contain_prevout(hash, verbosity):
586+
block = node.getblock(hash, verbosity)
585587
tx = block["tx"][1]
586588
if isinstance(tx, str):
587589
# In verbosity level 1, only the transaction hashes are written
@@ -595,24 +597,24 @@ def assert_vin_does_not_contain_prevout(verbosity):
595597
assert_hexblock_hashes(False)
596598

597599
self.log.info("Test that getblock with verbosity 1 doesn't include fee")
598-
assert_fee_not_in_block(1)
599-
assert_fee_not_in_block(True)
600+
assert_fee_not_in_block(blockhash, 1)
601+
assert_fee_not_in_block(blockhash, True)
600602

601603
self.log.info('Test that getblock with verbosity 2 and 3 includes expected fee')
602-
assert_fee_in_block(2)
603-
assert_fee_in_block(3)
604+
assert_fee_in_block(blockhash, 2)
605+
assert_fee_in_block(blockhash, 3)
604606

605607
self.log.info("Test that getblock with verbosity 1 and 2 does not include prevout")
606-
assert_vin_does_not_contain_prevout(1)
607-
assert_vin_does_not_contain_prevout(2)
608+
assert_vin_does_not_contain_prevout(blockhash, 1)
609+
assert_vin_does_not_contain_prevout(blockhash, 2)
608610

609611
self.log.info("Test that getblock with verbosity 3 includes prevout")
610612
assert_vin_contains_prevout(3)
611613

612614
self.log.info("Test getblock with invalid verbosity type returns proper error message")
613615
assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", node.getblock, blockhash, "2")
614616

615-
self.log.info("Test that getblock with verbosity 2 and 3 still works with pruned Undo data")
617+
self.log.info("Test that getblock doesn't work with deleted Undo data")
616618

617619
def move_block_file(old, new):
618620
old_path = self.nodes[0].blocks_path / old
@@ -622,10 +624,8 @@ def move_block_file(old, new):
622624
# Move instead of deleting so we can restore chain state afterwards
623625
move_block_file('rev00000.dat', 'rev_wrong')
624626

625-
assert_fee_not_in_block(2)
626-
assert_fee_not_in_block(3)
627-
assert_vin_does_not_contain_prevout(2)
628-
assert_vin_does_not_contain_prevout(3)
627+
assert_raises_rpc_error(-32603, "Undo data expected but can't be read. This could be due to disk corruption or a conflict with a pruning event.", lambda: node.getblock(blockhash, 2))
628+
assert_raises_rpc_error(-32603, "Undo data expected but can't be read. This could be due to disk corruption or a conflict with a pruning event.", lambda: node.getblock(blockhash, 3))
629629

630630
# Restore chain state
631631
move_block_file('rev_wrong', 'rev00000.dat')
@@ -641,6 +641,18 @@ def move_block_file(old, new):
641641
node.submitheader(block.serialize().hex())
642642
assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", lambda: node.getblock(block.hash))
643643

644+
self.log.info("Test getblock when block data is available but undo data isn't")
645+
# Submits a block building on the header-only block, so it can't be connected and has no undo data
646+
tx = create_tx_with_script(block.vtx[0], 0, script_sig=bytes([OP_TRUE]), amount=50 * COIN)
647+
block_noundo = create_block(block.sha256, create_coinbase(current_height + 2, nValue=100), block_time + 1, txlist=[tx])
648+
block_noundo.solve()
649+
node.submitblock(block_noundo.serialize().hex())
650+
651+
assert_fee_not_in_block(block_noundo.hash, 2)
652+
assert_fee_not_in_block(block_noundo.hash, 3)
653+
assert_vin_does_not_contain_prevout(block_noundo.hash, 2)
654+
assert_vin_does_not_contain_prevout(block_noundo.hash, 3)
655+
644656
self.log.info("Test getblock when block is missing")
645657
move_block_file('blk00000.dat', 'blk00000.dat.bak')
646658
assert_raises_rpc_error(-1, "Block not found on disk", node.getblock, blockhash)

0 commit comments

Comments
 (0)