Skip to content

Commit e983ed4

Browse files
committed
Merge bitcoin/bitcoin#30410: rpc, rest: Improve block rpc error handling, check header before attempting to read block data.
6a1aa51 rpc: check block index before reading block / undo data (Martin Zumsande) 6cbf2e5 rpc: Improve gettxoutproof error when only header is available. (Martin Zumsande) 69fc867 test: add coverage to getblock and getblockstats (Martin Zumsande) 5290cbd rpc: Improve getblock / getblockstats error when only header is available. (Martin Zumsande) e5b537b rest: improve error when only header of a block is available. (Martin Zumsande) Pull request description: Fixes #20978 If a block was pruned, `getblock` already returns a specific error: "Block not available (pruned data)". But if we haven't received the full block yet (e.g. in a race with block downloading after a new block was received headers-first, or during IBD) we just return an unspecific "Block not found on disk" error and log `ERROR: ReadBlockFromDisk: OpenBlockFile failed for FlatFilePos(nFile=-1, nPos=0) ` which suggest something went wrong even though this is a completely normal and expected situation. This PR improves the error message and stops calling `ReadRawBlockFromDisk()`, when we already know from the header that the block is not available on disk. Similarly, it prevents all other rpcs from calling blockstorage read functions unless we expect the data to be there, so that `LogError()` will only be thrown when there is an actual file system problem. I'm not completely sure if the cause is important enough to change the wording of the rpc error, that some scripts may rely on. If reviewers prefer it, an alternative solution would be to keep returning the current "Block not found on disk" error, but return it immediately instead of calling `ReadRawBlockFromDisk`, which would at least prevent the log error and also be an improvement in my opinion. ACKs for top commit: fjahr: re-ACK 6a1aa51 achow101: ACK 6a1aa51 andrewtoth: re-ACK 6a1aa51 Tree-SHA512: 491aef880e8298a05841c4bf8eb913ef84820d1ad5415fd17d9b441bff181959ebfdd432b5eb8347dc9c568433f9a2384ca9d84cd72c79d8a58323ca117538fe
2 parents fce9e06 + 6a1aa51 commit e983ed4

12 files changed

+113
-50
lines changed

src/rest.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,11 @@ static bool rest_block(const std::any& context,
309309
if (!pblockindex) {
310310
return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not found");
311311
}
312-
if (chainman.m_blockman.IsBlockPruned(*pblockindex)) {
313-
return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not available (pruned data)");
312+
if (!(pblockindex->nStatus & BLOCK_HAVE_DATA)) {
313+
if (chainman.m_blockman.IsBlockPruned(*pblockindex)) {
314+
return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not available (pruned data)");
315+
}
316+
return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not available (not fully downloaded)");
314317
}
315318
pos = pblockindex->GetBlockPos();
316319
}

src/rpc/blockchain.cpp

Lines changed: 26 additions & 17 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
@@ -597,20 +599,32 @@ static RPCHelpMan getblockheader()
597599
};
598600
}
599601

602+
void CheckBlockDataAvailability(BlockManager& blockman, const CBlockIndex& blockindex, bool check_for_undo)
603+
{
604+
AssertLockHeld(cs_main);
605+
uint32_t flag = check_for_undo ? BLOCK_HAVE_UNDO : BLOCK_HAVE_DATA;
606+
if (!(blockindex.nStatus & flag)) {
607+
if (blockman.IsBlockPruned(blockindex)) {
608+
throw JSONRPCError(RPC_MISC_ERROR, strprintf("%s not available (pruned data)", check_for_undo ? "Undo data" : "Block"));
609+
}
610+
if (check_for_undo) {
611+
throw JSONRPCError(RPC_MISC_ERROR, "Undo data not available");
612+
}
613+
throw JSONRPCError(RPC_MISC_ERROR, "Block not available (not fully downloaded)");
614+
}
615+
}
616+
600617
static CBlock GetBlockChecked(BlockManager& blockman, const CBlockIndex& blockindex)
601618
{
602619
CBlock block;
603620
{
604621
LOCK(cs_main);
605-
if (blockman.IsBlockPruned(blockindex)) {
606-
throw JSONRPCError(RPC_MISC_ERROR, "Block not available (pruned data)");
607-
}
622+
CheckBlockDataAvailability(blockman, blockindex, /*check_for_undo=*/false);
608623
}
609624

610625
if (!blockman.ReadBlockFromDisk(block, blockindex)) {
611-
// Block not found on disk. This could be because we have the block
612-
// header in our index but not yet have the block or did not accept the
613-
// block. Or if the block was pruned right after we released the lock above.
626+
// Block not found on disk. This shouldn't normally happen unless the block was
627+
// pruned right after we released the lock above.
614628
throw JSONRPCError(RPC_MISC_ERROR, "Block not found on disk");
615629
}
616630

@@ -623,16 +637,13 @@ static std::vector<uint8_t> GetRawBlockChecked(BlockManager& blockman, const CBl
623637
FlatFilePos pos{};
624638
{
625639
LOCK(cs_main);
626-
if (blockman.IsBlockPruned(blockindex)) {
627-
throw JSONRPCError(RPC_MISC_ERROR, "Block not available (pruned data)");
628-
}
640+
CheckBlockDataAvailability(blockman, blockindex, /*check_for_undo=*/false);
629641
pos = blockindex.GetBlockPos();
630642
}
631643

632644
if (!blockman.ReadRawBlockFromDisk(data, pos)) {
633-
// Block not found on disk. This could be because we have the block
634-
// header in our index but not yet have the block or did not accept the
635-
// block. Or if the block was pruned right after we released the lock above.
645+
// Block not found on disk. This shouldn't normally happen unless the block was
646+
// pruned right after we released the lock above.
636647
throw JSONRPCError(RPC_MISC_ERROR, "Block not found on disk");
637648
}
638649

@@ -648,9 +659,7 @@ static CBlockUndo GetUndoChecked(BlockManager& blockman, const CBlockIndex& bloc
648659

649660
{
650661
LOCK(cs_main);
651-
if (blockman.IsBlockPruned(blockindex)) {
652-
throw JSONRPCError(RPC_MISC_ERROR, "Undo data not available (pruned data)");
653-
}
662+
CheckBlockDataAvailability(blockman, blockindex, /*check_for_undo=*/true);
654663
}
655664

656665
if (!blockman.UndoReadFromDisk(blockUndo, blockindex)) {

src/rpc/blockchain.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,6 @@ UniValue CreateUTXOSnapshot(
6060

6161
//! Return height of highest block that has been pruned, or std::nullopt if no blocks have been pruned
6262
std::optional<int> GetPruneHeight(const node::BlockManager& blockman, const CChain& chain) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
63+
void CheckBlockDataAvailability(node::BlockManager& blockman, const CBlockIndex& blockindex, bool check_for_undo) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
6364

6465
#endif // BITCOIN_RPC_BLOCKCHAIN_H

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; });

src/rpc/txoutproof.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <merkleblock.h>
1111
#include <node/blockstorage.h>
1212
#include <primitives/transaction.h>
13+
#include <rpc/blockchain.h>
1314
#include <rpc/server.h>
1415
#include <rpc/server_util.h>
1516
#include <rpc/util.h>
@@ -96,6 +97,10 @@ static RPCHelpMan gettxoutproof()
9697
}
9798
}
9899

100+
{
101+
LOCK(cs_main);
102+
CheckBlockDataAvailability(chainman.m_blockman, *pblockindex, /*check_for_undo=*/false);
103+
}
99104
CBlock block;
100105
if (!chainman.m_blockman.ReadBlockFromDisk(block, *pblockindex)) {
101106
throw JSONRPCError(RPC_INTERNAL_ERROR, "Can't read block from disk");

test/functional/feature_assumeutxo.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ def test_sync_from_assumeutxo_node(self, snapshot):
313313
self.connect_nodes(snapshot_node.index, miner.index)
314314
self.sync_blocks(nodes=(miner, snapshot_node))
315315
# Check the base snapshot block was stored and ensure node signals full-node service support
316-
self.wait_until(lambda: not try_rpc(-1, "Block not found", snapshot_node.getblock, snapshot_block_hash))
316+
self.wait_until(lambda: not try_rpc(-1, "Block not available (not fully downloaded)", snapshot_node.getblock, snapshot_block_hash))
317317
self.wait_until(lambda: 'NETWORK' in snapshot_node.getnetworkinfo()['localservicesnames'])
318318

319319
# Now that the snapshot_node is synced, verify the ibd_node can sync from it
@@ -485,7 +485,7 @@ def check_dump_output(output):
485485
# find coinbase output at snapshot height on node0 and scan for it on node1,
486486
# where the block is not available, but the snapshot was loaded successfully
487487
coinbase_tx = n0.getblock(snapshot_hash, verbosity=2)['tx'][0]
488-
assert_raises_rpc_error(-1, "Block not found on disk", n1.getblock, snapshot_hash)
488+
assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", n1.getblock, snapshot_hash)
489489
coinbase_output_descriptor = coinbase_tx['vout'][0]['scriptPubKey']['desc']
490490
scan_result = n1.scantxoutset('start', [coinbase_output_descriptor])
491491
assert_equal(scan_result['success'], True)
@@ -557,7 +557,7 @@ def check_tx_counts(final: bool) -> None:
557557
self.log.info("Submit a spending transaction for a snapshot chainstate coin to the mempool")
558558
# spend the coinbase output of the first block that is not available on node1
559559
spend_coin_blockhash = n1.getblockhash(START_HEIGHT + 1)
560-
assert_raises_rpc_error(-1, "Block not found on disk", n1.getblock, spend_coin_blockhash)
560+
assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", n1.getblock, spend_coin_blockhash)
561561
prev_tx = n0.getblock(spend_coin_blockhash, 3)['tx'][0]
562562
prevout = {"txid": prev_tx['txid'], "vout": 0, "scriptPubKey": prev_tx['vout'][0]['scriptPubKey']['hex']}
563563
privkey = n0.get_deterministic_priv_key().key

test/functional/p2p_node_network_limited.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,10 @@ def test_avoid_requesting_historical_blocks(self):
102102
tip_height = pruned_node.getblockcount()
103103
limit_buffer = 2
104104
# Prevent races by waiting for the tip to arrive first
105-
self.wait_until(lambda: not try_rpc(-1, "Block not found", full_node.getblock, pruned_node.getbestblockhash()))
105+
self.wait_until(lambda: not try_rpc(-1, "Block not available (not fully downloaded)", full_node.getblock, pruned_node.getbestblockhash()))
106106
for height in range(start_height_full_node + 1, tip_height + 1):
107107
if height <= tip_height - (NODE_NETWORK_LIMITED_MIN_BLOCKS - limit_buffer):
108-
assert_raises_rpc_error(-1, "Block not found on disk", full_node.getblock, pruned_node.getblockhash(height))
108+
assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", full_node.getblock, pruned_node.getblockhash(height))
109109
else:
110110
full_node.getblock(pruned_node.getblockhash(height)) # just assert it does not throw an exception
111111

test/functional/p2p_unrequested_blocks.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def run_test(self):
119119
assert_equal(x['status'], "headers-only")
120120
tip_entry_found = True
121121
assert tip_entry_found
122-
assert_raises_rpc_error(-1, "Block not found on disk", self.nodes[0].getblock, block_h1f.hash)
122+
assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", self.nodes[0].getblock, block_h1f.hash)
123123

124124
# 4. Send another two block that build on the fork.
125125
block_h2f = create_block(block_h1f.sha256, create_coinbase(2), block_time)
@@ -191,7 +191,7 @@ def run_test(self):
191191
# Blocks 1-287 should be accepted, block 288 should be ignored because it's too far ahead
192192
for x in all_blocks[:-1]:
193193
self.nodes[0].getblock(x.hash)
194-
assert_raises_rpc_error(-1, "Block not found on disk", self.nodes[0].getblock, all_blocks[-1].hash)
194+
assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", self.nodes[0].getblock, all_blocks[-1].hash)
195195

196196
# 5. Test handling of unrequested block on the node that didn't process
197197
# Should still not be processed (even though it has a child that has more
@@ -230,7 +230,7 @@ def run_test(self):
230230
assert_equal(self.nodes[0].getblockcount(), 290)
231231
self.nodes[0].getblock(all_blocks[286].hash)
232232
assert_equal(self.nodes[0].getbestblockhash(), all_blocks[286].hash)
233-
assert_raises_rpc_error(-1, "Block not found on disk", self.nodes[0].getblock, all_blocks[287].hash)
233+
assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", self.nodes[0].getblock, all_blocks[287].hash)
234234
self.log.info("Successfully reorged to longer chain")
235235

236236
# 8. Create a chain which is invalid at a height longer than the
@@ -260,7 +260,7 @@ def run_test(self):
260260
assert_equal(x['status'], "headers-only")
261261
tip_entry_found = True
262262
assert tip_entry_found
263-
assert_raises_rpc_error(-1, "Block not found on disk", self.nodes[0].getblock, block_292.hash)
263+
assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", self.nodes[0].getblock, block_292.hash)
264264

265265
test_node.send_message(msg_block(block_289f))
266266
test_node.send_and_ping(msg_block(block_290f))

test/functional/rpc_blockchain.py

Lines changed: 43 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,17 +624,40 @@ 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')
632632

633633
assert 'previousblockhash' not in node.getblock(node.getblockhash(0))
634634
assert 'nextblockhash' not in node.getblock(node.getbestblockhash())
635635

636+
self.log.info("Test getblock when only header is known")
637+
current_height = node.getblock(node.getbestblockhash())['height']
638+
block_time = node.getblock(node.getbestblockhash())['time'] + 1
639+
block = create_block(int(blockhash, 16), create_coinbase(current_height + 1, nValue=100), block_time)
640+
block.solve()
641+
node.submitheader(block.serialize().hex())
642+
assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", lambda: node.getblock(block.hash))
643+
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+
656+
self.log.info("Test getblock when block is missing")
657+
move_block_file('blk00000.dat', 'blk00000.dat.bak')
658+
assert_raises_rpc_error(-1, "Block not found on disk", node.getblock, blockhash)
659+
move_block_file('blk00000.dat.bak', 'blk00000.dat')
660+
636661

637662
if __name__ == '__main__':
638663
BlockchainTest(__file__).main()

test/functional/rpc_getblockfrompeer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def run_test(self):
5858
self.log.info("Node 0 should only have the header for node 1's block 3")
5959
x = next(filter(lambda x: x['hash'] == short_tip, self.nodes[0].getchaintips()))
6060
assert_equal(x['status'], "headers-only")
61-
assert_raises_rpc_error(-1, "Block not found on disk", self.nodes[0].getblock, short_tip)
61+
assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", self.nodes[0].getblock, short_tip)
6262

6363
self.log.info("Fetch block from node 1")
6464
peers = self.nodes[0].getpeerinfo()

0 commit comments

Comments
 (0)