Merge bitcoin/bitcoin#32646: p2p: Add witness mutation check inside FillBlock

28299ce776 p2p: remove vestigial READ_STATUS_CHECKBLOCK_FAILED (Greg Sanders)
bac9ee4830 p2p: Add witness mutation check inside FillBlock (Greg Sanders)

Pull request description:

  Since #29412, we have not allowed mutated blocks to continue being processed immediately the block is received, but this is only done for the legacy BLOCK message.

  Extend these checks as belt-and-suspenders to not allow similar mutation strategies to affect relay by honest peers by applying the check inside `PartiallyDownloadedBlock::FillBlock`, immediately before returning `READ_STATUS_OK`.

ACKs for top commit:
  Crypt-iQ:
    ACK 28299ce776
  achow101:
    ACK 28299ce776
  stratospher:
    ACK 28299ce7.
  dergoegge:
    Code review ACK 28299ce776

Tree-SHA512: 883d7c12ca096234b425e6fe12e46b0611607600916e6ac8d1c8112224aa76924b7b074754910163ac2ec15379075d618a9ece3642649ac7629cddb0d4e432ea
This commit is contained in:
merge-script committed 2025-06-30 13:15:37 -04:00
commit f5f3e1f263
5 files changed
+38 -71

No files matched your search

+6 -11
View File
@@ -179,7 +179,7 @@ bool PartiallyDownloadedBlock::IsTxAvailable(size_t index) const
return txn_available[index] != nullptr; return txn_available[index] != nullptr;
} }
ReadStatus PartiallyDownloadedBlock::FillBlock(CBlock& block, const std::vector<CTransactionRef>& vtx_missing) ReadStatus PartiallyDownloadedBlock::FillBlock(CBlock& block, const std::vector<CTransactionRef>& vtx_missing, bool segwit_active)
{ {
if (header.IsNull()) return READ_STATUS_INVALID; if (header.IsNull()) return READ_STATUS_INVALID;
@@ -206,16 +206,11 @@ ReadStatus PartiallyDownloadedBlock::FillBlock(CBlock& block, const std::vector<
if (vtx_missing.size() != tx_missing_offset) if (vtx_missing.size() != tx_missing_offset)
return READ_STATUS_INVALID; return READ_STATUS_INVALID;
BlockValidationState state; // Check for possible mutations early now that we have a seemingly good block
CheckBlockFn check_block = m_check_block_mock ? m_check_block_mock : CheckBlock; IsBlockMutatedFn check_mutated{m_check_block_mutated_mock ? m_check_block_mutated_mock : IsBlockMutated};
if (!check_block(block, state, Params().GetConsensus(), /*fCheckPoW=*/true, /*fCheckMerkleRoot=*/true)) { if (check_mutated(/*block=*/block,
// TODO: We really want to just check merkle tree manually here, /*check_witness_root=*/segwit_active)) {
// but that is expensive, and CheckBlock caches a block's return READ_STATUS_FAILED; // Possible Short ID collision
// "checked-status" (in the CBlock?). CBlock should be able to
// check its own merkle root and cache that check.
if (state.GetResult() == BlockValidationResult::BLOCK_MUTATED)
return READ_STATUS_FAILED; // Possible Short ID collision
return READ_STATUS_CHECKBLOCK_FAILED;
} }
LogDebug(BCLog::CMPCTBLOCK, "Successfully reconstructed block %s with %u txn prefilled, %u txn from mempool (incl at least %u from extra pool) and %u txn (%u bytes) requested\n", hash.ToString(), prefilled_count, mempool_count, extra_count, vtx_missing.size(), tx_missing_size); LogDebug(BCLog::CMPCTBLOCK, "Successfully reconstructed block %s with %u txn prefilled, %u txn from mempool (incl at least %u from extra pool) and %u txn (%u bytes) requested\n", hash.ToString(), prefilled_count, mempool_count, extra_count, vtx_missing.size(), tx_missing_size);
+4 -5
View File
@@ -84,8 +84,6 @@ typedef enum ReadStatus_t
READ_STATUS_OK, READ_STATUS_OK,
READ_STATUS_INVALID, // Invalid object, peer is sending bogus crap READ_STATUS_INVALID, // Invalid object, peer is sending bogus crap
READ_STATUS_FAILED, // Failed to process object READ_STATUS_FAILED, // Failed to process object
READ_STATUS_CHECKBLOCK_FAILED, // Used only by FillBlock to indicate a
// failure in CheckBlock.
} ReadStatus; } ReadStatus;
class CBlockHeaderAndShortTxIDs { class CBlockHeaderAndShortTxIDs {
@@ -141,15 +139,16 @@ public:
CBlockHeader header; CBlockHeader header;
// Can be overridden for testing // Can be overridden for testing
using CheckBlockFn = std::function<bool(const CBlock&, BlockValidationState&, const Consensus::Params&, bool, bool)>; using IsBlockMutatedFn = std::function<bool(const CBlock&, bool)>;
CheckBlockFn m_check_block_mock{nullptr}; IsBlockMutatedFn m_check_block_mutated_mock{nullptr};
explicit PartiallyDownloadedBlock(CTxMemPool* poolIn) : pool(poolIn) {} explicit PartiallyDownloadedBlock(CTxMemPool* poolIn) : pool(poolIn) {}
// extra_txn is a list of extra orphan/conflicted/etc transactions to look at // extra_txn is a list of extra orphan/conflicted/etc transactions to look at
ReadStatus InitData(const CBlockHeaderAndShortTxIDs& cmpctblock, const std::vector<CTransactionRef>& extra_txn); ReadStatus InitData(const CBlockHeaderAndShortTxIDs& cmpctblock, const std::vector<CTransactionRef>& extra_txn);
bool IsTxAvailable(size_t index) const; bool IsTxAvailable(size_t index) const;
ReadStatus FillBlock(CBlock& block, const std::vector<CTransactionRef>& vtx_missing); // segwit_active enforces witness mutation checks just before reporting a healthy status
ReadStatus FillBlock(CBlock& block, const std::vector<CTransactionRef>& vtx_missing, bool segwit_active);
}; };
#endif // BITCOIN_BLOCKENCODINGS_H #endif // BITCOIN_BLOCKENCODINGS_H
+9 -19
View File
@@ -3360,7 +3360,11 @@ void PeerManagerImpl::ProcessCompactBlockTxns(CNode& pfrom, Peer& peer, const Bl
} }
PartiallyDownloadedBlock& partialBlock = *range_flight.first->second.second->partialBlock; PartiallyDownloadedBlock& partialBlock = *range_flight.first->second.second->partialBlock;
ReadStatus status = partialBlock.FillBlock(*pblock, block_transactions.txn);
// We should not have gotten this far in compact block processing unless it's attached to a known header
const CBlockIndex* prev_block{Assume(m_chainman.m_blockman.LookupBlockIndex(partialBlock.header.hashPrevBlock))};
ReadStatus status = partialBlock.FillBlock(*pblock, block_transactions.txn,
/*segwit_active=*/DeploymentActiveAfter(prev_block, m_chainman, Consensus::DEPLOYMENT_SEGWIT));
if (status == READ_STATUS_INVALID) { if (status == READ_STATUS_INVALID) {
RemoveBlockRequest(block_transactions.blockhash, pfrom.GetId()); // Reset in-flight state in case Misbehaving does not result in a disconnect RemoveBlockRequest(block_transactions.blockhash, pfrom.GetId()); // Reset in-flight state in case Misbehaving does not result in a disconnect
Misbehaving(peer, "invalid compact block/non-matching block transactions"); Misbehaving(peer, "invalid compact block/non-matching block transactions");
@@ -3377,23 +3381,7 @@ void PeerManagerImpl::ProcessCompactBlockTxns(CNode& pfrom, Peer& peer, const Bl
return; return;
} }
} else { } else {
// Block is either okay, or possibly we received // Block is okay for further processing
// READ_STATUS_CHECKBLOCK_FAILED.
// Note that CheckBlock can only fail for one of a few reasons:
// 1. bad-proof-of-work (impossible here, because we've already
// accepted the header)
// 2. merkleroot doesn't match the transactions given (already
// caught in FillBlock with READ_STATUS_FAILED, so
// impossible here)
// 3. the block is otherwise invalid (eg invalid coinbase,
// block is too big, too many legacy sigops, etc).
// So if CheckBlock failed, #3 is the only possibility.
// Under BIP 152, we don't discourage the peer unless proof of work is
// invalid (we don't require all the stateless checks to have
// been run). This is handled below, so just treat this as
// though the block was successfully read, and rely on the
// handling in ProcessNewBlock to ensure the block index is
// updated, etc.
RemoveBlockRequest(block_transactions.blockhash, pfrom.GetId()); // it is now an empty pointer RemoveBlockRequest(block_transactions.blockhash, pfrom.GetId()); // it is now an empty pointer
fBlockRead = true; fBlockRead = true;
// mapBlockSource is used for potentially punishing peers and // mapBlockSource is used for potentially punishing peers and
@@ -4529,7 +4517,9 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
return; return;
} }
std::vector<CTransactionRef> dummy; std::vector<CTransactionRef> dummy;
status = tempBlock.FillBlock(*pblock, dummy); const CBlockIndex* prev_block{Assume(m_chainman.m_blockman.LookupBlockIndex(cmpctblock.header.hashPrevBlock))};
status = tempBlock.FillBlock(*pblock, dummy,
/*segwit_active=*/DeploymentActiveAfter(prev_block, m_chainman, Consensus::DEPLOYMENT_SEGWIT));
if (status == READ_STATUS_OK) { if (status == READ_STATUS_OK) {
fBlockReconstructed = true; fBlockReconstructed = true;
} }
+8 -8
View File
@@ -95,21 +95,21 @@ BOOST_AUTO_TEST_CASE(SimpleRoundTripTest)
CBlock block2; CBlock block2;
{ {
PartiallyDownloadedBlock tmp = partialBlock; PartiallyDownloadedBlock tmp = partialBlock;
BOOST_CHECK(partialBlock.FillBlock(block2, {}) == READ_STATUS_INVALID); // No transactions BOOST_CHECK(partialBlock.FillBlock(block2, {}, /*segwit_active=*/true) == READ_STATUS_INVALID); // No transactions
partialBlock = tmp; partialBlock = tmp;
} }
// Wrong transaction // Wrong transaction
{ {
PartiallyDownloadedBlock tmp = partialBlock; PartiallyDownloadedBlock tmp = partialBlock;
partialBlock.FillBlock(block2, {block.vtx[2]}); // Current implementation doesn't check txn here, but don't require that partialBlock.FillBlock(block2, {block.vtx[2]}, /*segwit_active=*/true); // Current implementation doesn't check txn here, but don't require that
partialBlock = tmp; partialBlock = tmp;
} }
bool mutated; bool mutated;
BOOST_CHECK(block.hashMerkleRoot != BlockMerkleRoot(block2, &mutated)); BOOST_CHECK(block.hashMerkleRoot != BlockMerkleRoot(block2, &mutated));
CBlock block3; CBlock block3;
BOOST_CHECK(partialBlock.FillBlock(block3, {block.vtx[1]}) == READ_STATUS_OK); BOOST_CHECK(partialBlock.FillBlock(block3, {block.vtx[1]}, /*segwit_active=*/true) == READ_STATUS_OK);
BOOST_CHECK_EQUAL(block.GetHash().ToString(), block3.GetHash().ToString()); BOOST_CHECK_EQUAL(block.GetHash().ToString(), block3.GetHash().ToString());
BOOST_CHECK_EQUAL(block.hashMerkleRoot.ToString(), BlockMerkleRoot(block3, &mutated).ToString()); BOOST_CHECK_EQUAL(block.hashMerkleRoot.ToString(), BlockMerkleRoot(block3, &mutated).ToString());
BOOST_CHECK(!mutated); BOOST_CHECK(!mutated);
@@ -182,14 +182,14 @@ BOOST_AUTO_TEST_CASE(NonCoinbasePreforwardRTTest)
CBlock block2; CBlock block2;
{ {
PartiallyDownloadedBlock tmp = partialBlock; PartiallyDownloadedBlock tmp = partialBlock;
BOOST_CHECK(partialBlock.FillBlock(block2, {}) == READ_STATUS_INVALID); // No transactions BOOST_CHECK(partialBlock.FillBlock(block2, {}, /*segwit_active=*/true) == READ_STATUS_INVALID); // No transactions
partialBlock = tmp; partialBlock = tmp;
} }
// Wrong transaction // Wrong transaction
{ {
PartiallyDownloadedBlock tmp = partialBlock; PartiallyDownloadedBlock tmp = partialBlock;
partialBlock.FillBlock(block2, {block.vtx[1]}); // Current implementation doesn't check txn here, but don't require that partialBlock.FillBlock(block2, {block.vtx[1]}, /*segwit_active=*/true); // Current implementation doesn't check txn here, but don't require that
partialBlock = tmp; partialBlock = tmp;
} }
BOOST_CHECK_EQUAL(pool.get(block.vtx[2]->GetHash()).use_count(), SHARED_TX_OFFSET + 2); // +2 because of partialBlock and block2 BOOST_CHECK_EQUAL(pool.get(block.vtx[2]->GetHash()).use_count(), SHARED_TX_OFFSET + 2); // +2 because of partialBlock and block2
@@ -198,7 +198,7 @@ BOOST_AUTO_TEST_CASE(NonCoinbasePreforwardRTTest)
CBlock block3; CBlock block3;
PartiallyDownloadedBlock partialBlockCopy = partialBlock; PartiallyDownloadedBlock partialBlockCopy = partialBlock;
BOOST_CHECK(partialBlock.FillBlock(block3, {block.vtx[0]}) == READ_STATUS_OK); BOOST_CHECK(partialBlock.FillBlock(block3, {block.vtx[0]}, /*segwit_active=*/true) == READ_STATUS_OK);
BOOST_CHECK_EQUAL(block.GetHash().ToString(), block3.GetHash().ToString()); BOOST_CHECK_EQUAL(block.GetHash().ToString(), block3.GetHash().ToString());
BOOST_CHECK_EQUAL(block.hashMerkleRoot.ToString(), BlockMerkleRoot(block3, &mutated).ToString()); BOOST_CHECK_EQUAL(block.hashMerkleRoot.ToString(), BlockMerkleRoot(block3, &mutated).ToString());
BOOST_CHECK(!mutated); BOOST_CHECK(!mutated);
@@ -252,7 +252,7 @@ BOOST_AUTO_TEST_CASE(SufficientPreforwardRTTest)
CBlock block2; CBlock block2;
PartiallyDownloadedBlock partialBlockCopy = partialBlock; PartiallyDownloadedBlock partialBlockCopy = partialBlock;
BOOST_CHECK(partialBlock.FillBlock(block2, {}) == READ_STATUS_OK); BOOST_CHECK(partialBlock.FillBlock(block2, {}, /*segwit_active=*/true) == READ_STATUS_OK);
BOOST_CHECK_EQUAL(block.GetHash().ToString(), block2.GetHash().ToString()); BOOST_CHECK_EQUAL(block.GetHash().ToString(), block2.GetHash().ToString());
bool mutated; bool mutated;
BOOST_CHECK_EQUAL(block.hashMerkleRoot.ToString(), BlockMerkleRoot(block2, &mutated).ToString()); BOOST_CHECK_EQUAL(block.hashMerkleRoot.ToString(), BlockMerkleRoot(block2, &mutated).ToString());
@@ -300,7 +300,7 @@ BOOST_AUTO_TEST_CASE(EmptyBlockRoundTripTest)
CBlock block2; CBlock block2;
std::vector<CTransactionRef> vtx_missing; std::vector<CTransactionRef> vtx_missing;
BOOST_CHECK(partialBlock.FillBlock(block2, vtx_missing) == READ_STATUS_OK); BOOST_CHECK(partialBlock.FillBlock(block2, vtx_missing, /*segwit_active=*/true) == READ_STATUS_OK);
BOOST_CHECK_EQUAL(block.GetHash().ToString(), block2.GetHash().ToString()); BOOST_CHECK_EQUAL(block.GetHash().ToString(), block2.GetHash().ToString());
BOOST_CHECK_EQUAL(block.hashMerkleRoot.ToString(), BlockMerkleRoot(block2, &mutated).ToString()); BOOST_CHECK_EQUAL(block.hashMerkleRoot.ToString(), BlockMerkleRoot(block2, &mutated).ToString());
BOOST_CHECK(!mutated); BOOST_CHECK(!mutated);
+11 -28
View File
@@ -36,14 +36,10 @@ void initialize_pdb()
g_setup = testing_setup.get(); g_setup = testing_setup.get();
} }
PartiallyDownloadedBlock::CheckBlockFn FuzzedCheckBlock(std::optional<BlockValidationResult> result) PartiallyDownloadedBlock::IsBlockMutatedFn FuzzedIsBlockMutated(bool result)
{ {
return [result](const CBlock&, BlockValidationState& state, const Consensus::Params&, bool, bool) { return [result](const CBlock& block, bool) {
if (result) { return result;
return state.Invalid(*result);
}
return true;
}; };
} }
@@ -115,35 +111,22 @@ FUZZ_TARGET(partially_downloaded_block, .init = initialize_pdb)
skipped_missing |= (!pdb.IsTxAvailable(i) && skip); skipped_missing |= (!pdb.IsTxAvailable(i) && skip);
} }
// Mock CheckBlock bool segwit_active{fuzzed_data_provider.ConsumeBool()};
bool fail_check_block{fuzzed_data_provider.ConsumeBool()};
auto validation_result = // Mock IsBlockMutated
fuzzed_data_provider.PickValueInArray( bool fail_block_mutated{fuzzed_data_provider.ConsumeBool()};
{BlockValidationResult::BLOCK_RESULT_UNSET, pdb.m_check_block_mutated_mock = FuzzedIsBlockMutated(fail_block_mutated);
BlockValidationResult::BLOCK_CONSENSUS,
BlockValidationResult::BLOCK_CACHED_INVALID,
BlockValidationResult::BLOCK_INVALID_HEADER,
BlockValidationResult::BLOCK_MUTATED,
BlockValidationResult::BLOCK_MISSING_PREV,
BlockValidationResult::BLOCK_INVALID_PREV,
BlockValidationResult::BLOCK_TIME_FUTURE,
BlockValidationResult::BLOCK_HEADER_LOW_WORK});
pdb.m_check_block_mock = FuzzedCheckBlock(
fail_check_block ?
std::optional<BlockValidationResult>{validation_result} :
std::nullopt);
CBlock reconstructed_block; CBlock reconstructed_block;
auto fill_status{pdb.FillBlock(reconstructed_block, missing)}; auto fill_status{pdb.FillBlock(reconstructed_block, missing, segwit_active)};
switch (fill_status) { switch (fill_status) {
case READ_STATUS_OK: case READ_STATUS_OK:
assert(!skipped_missing); assert(!skipped_missing);
assert(!fail_check_block); assert(!fail_block_mutated);
assert(block->GetHash() == reconstructed_block.GetHash()); assert(block->GetHash() == reconstructed_block.GetHash());
break; break;
case READ_STATUS_CHECKBLOCK_FAILED: [[fallthrough]];
case READ_STATUS_FAILED: case READ_STATUS_FAILED:
assert(fail_check_block); assert(fail_block_mutated);
break; break;
case READ_STATUS_INVALID: case READ_STATUS_INVALID:
break; break;