Merge bitcoin/bitcoin#32646: p2p: Add witness mutation check inside FillBlock
28299ce776p2p: remove vestigial READ_STATUS_CHECKBLOCK_FAILED (Greg Sanders)bac9ee4830p2p: 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: ACK28299ce776achow101: ACK28299ce776stratospher: ACK28299ce7. dergoegge: Code review ACK28299ce776Tree-SHA512: 883d7c12ca096234b425e6fe12e46b0611607600916e6ac8d1c8112224aa76924b7b074754910163ac2ec15379075d618a9ece3642649ac7629cddb0d4e432ea
This commit is contained in:
5 files changed
+38
-71
No files matched your search
+6
-11
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user