test: characterize extra transaction miscount
What changed, and why it matters
This commit adds a test case to Bitcoin Core that demonstrates a minor accounting bug in how compact block reconstruction counts 'extra' transactions. The test shows that when a fake transaction in the extra pool happens to have the same short ID as a real block transaction, the internal extra_count counter is reset to zero instead of staying at one. The commit message and a TODO comment make clear this is only a test characterizing the miscount, not a fix for a security vulnerability. There is no evidence this miscount can be exploited to steal funds, crash nodes, or break consensus.
Treat as a non-security test commit. If reviewing for release, note the characterized miscount and consider whether a follow-up production fix is warranted, but do not flag this commit itself as a vulnerability. Monitor upstream for any subsequent fix that changes PartiallyDownloadedBlock logic.
Security signals we found
Test-only change; no production code modified
TODO comment indicates known miscount, not a fix
Compact block shorttxid collision behavior is characterized
No consensus, mempool DoS, or funds-loss mechanism shown
Evidence from the diff
The change extends src/test/blockencodings_tests.cpp. It introduces a TestPartiallyDownloadedBlock helper exposing the protected extra_count field, then augments the ReceiveWithExtraTransactions test. The test builds a block with an extra transaction, seeds extra_txn with unrelated transactions, and verifies normal reconstruction. It then injects a collision: extra_txn[2] is set to a non-block transaction whose Wtxid collides with block.vtx[2]’s shorttxid. After InitData, the real block transaction at index 2 is unavailable (because the collision consumed the slot), and extra_count is 0U even though one unrelated extra transaction was already used. The TODO explicitly states ‘This should be 1’, confirming the commit is documenting/characterizing the bug rather than fixing it.
Changed components
src/test/blockencodings_tests.cppCompact block reconstruction test coveragePartiallyDownloadedBlock extra_count accountingInspect captured patch +21 / −0
diff --git a/src/test/blockencodings_tests.cpp b/src/test/blockencodings_tests.cpp
index e4200cac..954febb0 100644
--- a/src/test/blockencodings_tests.cpp
+++ b/src/test/blockencodings_tests.cpp
@@ -144,6 +144,12 @@ public:
SERIALIZE_METHODS(TestHeaderAndShortIDs, obj) { READWRITE(obj.header, obj.nonce, Using<VectorFormatter<CustomUintFormatter<CBlockHeaderAndShortTxIDs::SHORTTXIDS_LENGTH>>>(obj.shorttxids), obj.prefilledtxn); }
};
+struct TestPartiallyDownloadedBlock : PartiallyDownloadedBlock {
+ using PartiallyDownloadedBlock::PartiallyDownloadedBlock;
+
+ size_t GetExtraCount() const { return extra_count; }
+};
+
BOOST_AUTO_TEST_CASE(NonCoinbasePreforwardRTTest)
{
CTxMemPool& pool = *Assert(m_node.mempool);
@@ -319,6 +325,13 @@ BOOST_AUTO_TEST_CASE(ReceiveWithExtraTransactions) {
const CTransactionRef non_block_tx = MakeTransactionRef(std::move(mtx));
CBlock block(BuildBlockTestCase(rand_ctx));
+ // Leave one transaction missing so scanning doesn't stop before the collision.
+ mtx = BuildTransactionTestCase();
+ mtx.vin[0].prevout.hash = Txid::FromUint256(rand_ctx.rand256());
+ block.vtx.push_back(MakeTransactionRef(std::move(mtx)));
+ block.hashMerkleRoot = BlockMerkleRoot(block);
+ while (!CheckProofOfWork(block.GetHash(), block.nBits, Params().GetConsensus())) ++block.nNonce;
+
std::vector<std::pair<Wtxid, CTransactionRef>> extra_txn;
extra_txn.resize(10);
@@ -352,6 +365,14 @@ BOOST_AUTO_TEST_CASE(ReceiveWithExtraTransactions) {
// This transaction is now available via extra_txn:
BOOST_CHECK(partial_block_with_extra.IsTxAvailable(1));
BOOST_CHECK(partial_block_with_extra.IsTxAvailable(2));
+
+ // Simulate a mempool collision after finding an unrelated extra transaction.
+ extra_txn[2] = {block.vtx[2]->GetWitnessHash(), non_block_tx};
+ TestPartiallyDownloadedBlock partial_block_with_extra_collision{&pool};
+ BOOST_CHECK_EQUAL(partial_block_with_extra_collision.InitData(cmpctblock, extra_txn), READ_STATUS_OK);
+ BOOST_CHECK(partial_block_with_extra_collision.IsTxAvailable(1));
+ BOOST_CHECK(!partial_block_with_extra_collision.IsTxAvailable(2));
+ BOOST_CHECK_EQUAL(partial_block_with_extra_collision.GetExtraCount(), 0U); // TODO: This should be 1
}
}
Why this scored 26/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.