From 4e2af1c06547230b9245d94e7bcb1129f2c49714 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Sat, 6 Dec 2025 15:02:29 +0100 Subject: [PATCH] blockstorage: allow reading partial block data from storage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It will allow fetching specific transactions using an external index, following https://github.com/bitcoin/bitcoin/pull/32541#issuecomment-3267485313. No logging takes place in case of an invalid offset/size (to avoid spamming the log), by using a new `ReadRawError::BadPartRange` error variant. Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com> Co-authored-by: Lőrinc --- src/node/blockstorage.cpp | 11 +++++- src/node/blockstorage.h | 3 +- src/rest.cpp | 1 + src/test/blockmanager_tests.cpp | 62 +++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 0927eb852da..5ba00fcf4d6 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -1048,7 +1048,7 @@ bool BlockManager::ReadBlock(CBlock& block, const CBlockIndex& index) const return ReadBlock(block, block_pos, index.GetBlockHash()); } -BlockManager::ReadRawBlockResult BlockManager::ReadRawBlock(const FlatFilePos& pos) const +BlockManager::ReadRawBlockResult BlockManager::ReadRawBlock(const FlatFilePos& pos, std::optional> block_part) const { if (pos.nPos < STORAGE_HEADER_BYTES) { // If nPos is less than STORAGE_HEADER_BYTES, we can't read the header that precedes the block data @@ -1081,6 +1081,15 @@ BlockManager::ReadRawBlockResult BlockManager::ReadRawBlock(const FlatFilePos& p return util::Unexpected{ReadRawError::IO}; } + if (block_part) { + const auto [offset, size]{*block_part}; + if (size == 0 || offset >= blk_size || size > blk_size - offset) { + return util::Unexpected{ReadRawError::BadPartRange}; // Avoid logging - offset/size come from untrusted REST input + } + filein.seek(offset, SEEK_CUR); + blk_size = size; + } + std::vector data(blk_size); // Zeroing of memory is intentional here filein.read(data); return data; diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h index 1ce7e8f692c..e3f9c445ead 100644 --- a/src/node/blockstorage.h +++ b/src/node/blockstorage.h @@ -172,6 +172,7 @@ std::ostream& operator<<(std::ostream& os, const BlockfileCursor& cursor); enum class ReadRawError { IO, + BadPartRange, }; /** @@ -460,7 +461,7 @@ public: /** Functions for disk access for blocks */ bool ReadBlock(CBlock& block, const FlatFilePos& pos, const std::optional& expected_hash) const; bool ReadBlock(CBlock& block, const CBlockIndex& index) const; - ReadRawBlockResult ReadRawBlock(const FlatFilePos& pos) const; + ReadRawBlockResult ReadRawBlock(const FlatFilePos& pos, std::optional> block_part = std::nullopt) const; bool ReadBlockUndo(CBlockUndo& blockundo, const CBlockIndex& index) const; diff --git a/src/rest.cpp b/src/rest.cpp index 4883e26d676..26312633fdd 100644 --- a/src/rest.cpp +++ b/src/rest.cpp @@ -420,6 +420,7 @@ static bool rest_block(const std::any& context, if (!block_data) { switch (block_data.error()) { case node::ReadRawError::IO: return RESTERR(req, HTTP_INTERNAL_SERVER_ERROR, "I/O error reading " + hashStr); + case node::ReadRawError::BadPartRange: break; // can happen only when reading a block part } assert(false); } diff --git a/src/test/blockmanager_tests.cpp b/src/test/blockmanager_tests.cpp index a155199437b..4a326458464 100644 --- a/src/test/blockmanager_tests.cpp +++ b/src/test/blockmanager_tests.cpp @@ -138,6 +138,68 @@ BOOST_FIXTURE_TEST_CASE(blockmanager_block_data_availability, TestChain100Setup) BOOST_CHECK(!blockman.CheckBlockDataAvailability(tip, *last_pruned_block)); } +BOOST_FIXTURE_TEST_CASE(blockmanager_block_data_part, TestChain100Setup) +{ + LOCK(::cs_main); + auto& chainman{m_node.chainman}; + auto& blockman{chainman->m_blockman}; + const CBlockIndex& tip{*chainman->ActiveTip()}; + const FlatFilePos tip_block_pos{tip.GetBlockPos()}; + + auto block{blockman.ReadRawBlock(tip_block_pos)}; + BOOST_REQUIRE(block); + BOOST_REQUIRE_GE(block->size(), 200); + + const auto expect_part{[&](size_t offset, size_t size) { + auto res{blockman.ReadRawBlock(tip_block_pos, std::pair{offset, size})}; + BOOST_CHECK(res); + const auto& part{res.value()}; + BOOST_CHECK_EQUAL_COLLECTIONS(part.begin(), part.end(), block->begin() + offset, block->begin() + offset + size); + }}; + + expect_part(0, 20); + expect_part(0, block->size() - 1); + expect_part(0, block->size() - 10); + expect_part(0, block->size()); + expect_part(1, block->size() - 1); + expect_part(10, 20); + expect_part(block->size() - 1, 1); +} + +BOOST_FIXTURE_TEST_CASE(blockmanager_block_data_part_error, TestChain100Setup) +{ + LOCK(::cs_main); + auto& chainman{m_node.chainman}; + auto& blockman{chainman->m_blockman}; + const CBlockIndex& tip{*chainman->ActiveTip()}; + const FlatFilePos tip_block_pos{tip.GetBlockPos()}; + + auto block{blockman.ReadRawBlock(tip_block_pos)}; + BOOST_REQUIRE(block); + BOOST_REQUIRE_GE(block->size(), 200); + + const auto expect_part_error{[&](size_t offset, size_t size) { + auto res{blockman.ReadRawBlock(tip_block_pos, std::pair{offset, size})}; + BOOST_CHECK(!res); + BOOST_CHECK_EQUAL(res.error(), node::ReadRawError::BadPartRange); + }}; + + expect_part_error(0, 0); + expect_part_error(0, block->size() + 1); + expect_part_error(0, std::numeric_limits::max()); + expect_part_error(1, block->size()); + expect_part_error(2, block->size() - 1); + expect_part_error(block->size() - 1, 2); + expect_part_error(block->size() - 2, 3); + expect_part_error(block->size() + 1, 0); + expect_part_error(block->size() + 1, 1); + expect_part_error(block->size() + 2, 2); + expect_part_error(block->size(), 0); + expect_part_error(block->size(), 1); + expect_part_error(std::numeric_limits::max(), 1); + expect_part_error(std::numeric_limits::max(), std::numeric_limits::max()); +} + BOOST_FIXTURE_TEST_CASE(blockmanager_readblock_hash_mismatch, TestingSetup) { CBlockIndex index;