Skip to content

Commit 2649e65

Browse files
committed
Merge bitcoin/bitcoin#29412: p2p: Don't process mutated blocks
d8087ad [test] IsBlockMutated unit tests (dergoegge) 1ed2c98 Add transaction_identifier::size to allow Span conversion (dergoegge) 1ec6bbe [validation] Cache merkle root and witness commitment checks (dergoegge) 5bf4f5b [test] Add regression test for #27608 (dergoegge) 49257c0 [net processing] Don't process mutated blocks (dergoegge) 2d8495e [validation] Merkle root malleation should be caught by IsBlockMutated (dergoegge) 66abce1 [validation] Introduce IsBlockMutated (dergoegge) e7669e1 [refactor] Cleanup merkle root checks (dergoegge) 95bddb9 [validation] Isolate merkle root checks (dergoegge) Pull request description: This PR proposes to check for mutated blocks early as a defense-in-depth mitigation against attacks leveraging mutated blocks. We introduce `IsBlockMutated` which catches all known forms of block malleation and use it to do an early mutation check whenever we receive a `block` message. We have observed attacks that abused mutated blocks in the past, which could have been prevented by simply not processing mutated blocks (e.g. bitcoin/bitcoin#27608 for which a regression test is included in this PR). ACKs for top commit: achow101: ACK d8087ad maflcko: ACK d8087ad 🏄 fjahr: Code review ACK d8087ad sr-gi: Code review ACK bitcoin/bitcoin@d8087ad Tree-SHA512: 618ff4ea7f168e10f07504d3651290efbb1bb2ab3b838ffff3527c028caf6c52dedad18d04d3dbc627977479710930e200f2dfae18a08f627efe7e64a57e535f
2 parents 8e894be + d8087ad commit 2649e65

File tree

8 files changed

+449
-41
lines changed

8 files changed

+449
-41
lines changed

src/net_processing.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4719,6 +4719,16 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
47194719

47204720
LogPrint(BCLog::NET, "received block %s peer=%d\n", pblock->GetHash().ToString(), pfrom.GetId());
47214721

4722+
const CBlockIndex* prev_block{WITH_LOCK(m_chainman.GetMutex(), return m_chainman.m_blockman.LookupBlockIndex(pblock->hashPrevBlock))};
4723+
4724+
if (IsBlockMutated(/*block=*/*pblock,
4725+
/*check_witness_root=*/DeploymentActiveAfter(prev_block, m_chainman, Consensus::DEPLOYMENT_SEGWIT))) {
4726+
LogDebug(BCLog::NET, "Received mutated block from peer=%d\n", peer->m_id);
4727+
Misbehaving(*peer, 100, "mutated block");
4728+
WITH_LOCK(cs_main, RemoveBlockRequest(pblock->GetHash(), peer->m_id));
4729+
return;
4730+
}
4731+
47224732
bool forceProcessing = false;
47234733
const uint256 hash(pblock->GetHash());
47244734
bool min_pow_checked = false;
@@ -4734,7 +4744,6 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
47344744
mapBlockSource.emplace(hash, std::make_pair(pfrom.GetId(), true));
47354745

47364746
// Check work on this block against our anti-dos thresholds.
4737-
const CBlockIndex* prev_block = m_chainman.m_blockman.LookupBlockIndex(pblock->hashPrevBlock);
47384747
if (prev_block && prev_block->nChainWork + CalculateHeadersWork({pblock->GetBlockHeader()}) >= GetAntiDoSWorkThreshold()) {
47394748
min_pow_checked = true;
47404749
}

src/primitives/block.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,10 @@ class CBlock : public CBlockHeader
7171
// network and disk
7272
std::vector<CTransactionRef> vtx;
7373

74-
// memory only
75-
mutable bool fChecked;
74+
// Memory-only flags for caching expensive checks
75+
mutable bool fChecked; // CheckBlock()
76+
mutable bool m_checked_witness_commitment{false}; // CheckWitnessCommitment()
77+
mutable bool m_checked_merkle_root{false}; // CheckMerkleRoot()
7678

7779
CBlock()
7880
{
@@ -95,6 +97,8 @@ class CBlock : public CBlockHeader
9597
CBlockHeader::SetNull();
9698
vtx.clear();
9799
fChecked = false;
100+
m_checked_witness_commitment = false;
101+
m_checked_merkle_root = false;
98102
}
99103

100104
CBlockHeader GetBlockHeader() const

src/test/validation_tests.cpp

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44

55
#include <chainparams.h>
66
#include <consensus/amount.h>
7+
#include <consensus/merkle.h>
8+
#include <core_io.h>
9+
#include <hash.h>
710
#include <net.h>
811
#include <signet.h>
912
#include <uint256.h>
1013
#include <util/chaintype.h>
1114
#include <validation.h>
1215

16+
#include <string>
17+
1318
#include <test/util/setup_common.h>
1419

1520
#include <boost/test/unit_test.hpp>
@@ -145,4 +150,215 @@ BOOST_AUTO_TEST_CASE(test_assumeutxo)
145150
BOOST_CHECK_EQUAL(out110_2.nChainTx, 111U);
146151
}
147152

153+
BOOST_AUTO_TEST_CASE(block_malleation)
154+
{
155+
// Test utilities that calls `IsBlockMutated` and then clears the validity
156+
// cache flags on `CBlock`.
157+
auto is_mutated = [](CBlock& block, bool check_witness_root) {
158+
bool mutated{IsBlockMutated(block, check_witness_root)};
159+
block.fChecked = false;
160+
block.m_checked_witness_commitment = false;
161+
block.m_checked_merkle_root = false;
162+
return mutated;
163+
};
164+
auto is_not_mutated = [&is_mutated](CBlock& block, bool check_witness_root) {
165+
return !is_mutated(block, check_witness_root);
166+
};
167+
168+
// Test utilities to create coinbase transactions and insert witness
169+
// commitments.
170+
//
171+
// Note: this will not include the witness stack by default to avoid
172+
// triggering the "no witnesses allowed for blocks that don't commit to
173+
// witnesses" rule when testing other malleation vectors.
174+
auto create_coinbase_tx = [](bool include_witness = false) {
175+
CMutableTransaction coinbase;
176+
coinbase.vin.resize(1);
177+
if (include_witness) {
178+
coinbase.vin[0].scriptWitness.stack.resize(1);
179+
coinbase.vin[0].scriptWitness.stack[0] = std::vector<unsigned char>(32, 0x00);
180+
}
181+
182+
coinbase.vout.resize(1);
183+
coinbase.vout[0].scriptPubKey.resize(MINIMUM_WITNESS_COMMITMENT);
184+
coinbase.vout[0].scriptPubKey[0] = OP_RETURN;
185+
coinbase.vout[0].scriptPubKey[1] = 0x24;
186+
coinbase.vout[0].scriptPubKey[2] = 0xaa;
187+
coinbase.vout[0].scriptPubKey[3] = 0x21;
188+
coinbase.vout[0].scriptPubKey[4] = 0xa9;
189+
coinbase.vout[0].scriptPubKey[5] = 0xed;
190+
191+
auto tx = MakeTransactionRef(coinbase);
192+
assert(tx->IsCoinBase());
193+
return tx;
194+
};
195+
auto insert_witness_commitment = [](CBlock& block, uint256 commitment) {
196+
assert(!block.vtx.empty() && block.vtx[0]->IsCoinBase() && !block.vtx[0]->vout.empty());
197+
198+
CMutableTransaction mtx{*block.vtx[0]};
199+
CHash256().Write(commitment).Write(std::vector<unsigned char>(32, 0x00)).Finalize(commitment);
200+
memcpy(&mtx.vout[0].scriptPubKey[6], commitment.begin(), 32);
201+
block.vtx[0] = MakeTransactionRef(mtx);
202+
};
203+
204+
{
205+
CBlock block;
206+
207+
// Empty block is expected to have merkle root of 0x0.
208+
BOOST_CHECK(block.vtx.empty());
209+
block.hashMerkleRoot = uint256{1};
210+
BOOST_CHECK(is_mutated(block, /*check_witness_root=*/false));
211+
block.hashMerkleRoot = uint256{};
212+
BOOST_CHECK(is_not_mutated(block, /*check_witness_root=*/false));
213+
214+
// Block with a single coinbase tx is mutated if the merkle root is not
215+
// equal to the coinbase tx's hash.
216+
block.vtx.push_back(create_coinbase_tx());
217+
BOOST_CHECK(block.vtx[0]->GetHash() != block.hashMerkleRoot);
218+
BOOST_CHECK(is_mutated(block, /*check_witness_root=*/false));
219+
block.hashMerkleRoot = block.vtx[0]->GetHash();
220+
BOOST_CHECK(is_not_mutated(block, /*check_witness_root=*/false));
221+
222+
// Block with two transactions is mutated if the merkle root does not
223+
// match the double sha256 of the concatenation of the two transaction
224+
// hashes.
225+
block.vtx.push_back(MakeTransactionRef(CMutableTransaction{}));
226+
BOOST_CHECK(is_mutated(block, /*check_witness_root=*/false));
227+
HashWriter hasher;
228+
hasher.write(block.vtx[0]->GetHash());
229+
hasher.write(block.vtx[1]->GetHash());
230+
block.hashMerkleRoot = hasher.GetHash();
231+
BOOST_CHECK(is_not_mutated(block, /*check_witness_root=*/false));
232+
233+
// Block with two transactions is mutated if any node is duplicate.
234+
{
235+
block.vtx[1] = block.vtx[0];
236+
BOOST_CHECK(is_mutated(block, /*check_witness_root=*/false));
237+
HashWriter hasher;
238+
hasher.write(block.vtx[0]->GetHash());
239+
hasher.write(block.vtx[1]->GetHash());
240+
block.hashMerkleRoot = hasher.GetHash();
241+
BOOST_CHECK(is_mutated(block, /*check_witness_root=*/false));
242+
}
243+
244+
// Blocks with 64-byte coinbase transactions are not considered mutated
245+
block.vtx.clear();
246+
{
247+
CMutableTransaction mtx;
248+
mtx.vin.resize(1);
249+
mtx.vout.resize(1);
250+
mtx.vout[0].scriptPubKey.resize(4);
251+
block.vtx.push_back(MakeTransactionRef(mtx));
252+
block.hashMerkleRoot = block.vtx.back()->GetHash();
253+
assert(block.vtx.back()->IsCoinBase());
254+
assert(GetSerializeSize(TX_NO_WITNESS(block.vtx.back())) == 64);
255+
}
256+
BOOST_CHECK(is_not_mutated(block, /*check_witness_root=*/false));
257+
}
258+
259+
{
260+
// Test merkle root malleation
261+
262+
// Pseudo code to mine transactions tx{1,2,3}:
263+
//
264+
// ```
265+
// loop {
266+
// tx1 = random_tx()
267+
// tx2 = random_tx()
268+
// tx3 = deserialize_tx(txid(tx1) || txid(tx2));
269+
// if serialized_size_without_witness(tx3) == 64 {
270+
// print(hex(tx3))
271+
// break
272+
// }
273+
// }
274+
// ```
275+
//
276+
// The `random_tx` function used to mine the txs below simply created
277+
// empty transactions with a random version field.
278+
CMutableTransaction tx1;
279+
BOOST_CHECK(DecodeHexTx(tx1, "ff204bd0000000000000", /*try_no_witness=*/true, /*try_witness=*/false));
280+
CMutableTransaction tx2;
281+
BOOST_CHECK(DecodeHexTx(tx2, "8ae53c92000000000000", /*try_no_witness=*/true, /*try_witness=*/false));
282+
CMutableTransaction tx3;
283+
BOOST_CHECK(DecodeHexTx(tx3, "cdaf22d00002c6a7f848f8ae4d30054e61dcf3303d6fe01d282163341f06feecc10032b3160fcab87bdfe3ecfb769206ef2d991b92f8a268e423a6ef4d485f06", /*try_no_witness=*/true, /*try_witness=*/false));
284+
{
285+
// Verify that double_sha256(txid1||txid2) == txid3
286+
HashWriter hasher;
287+
hasher.write(tx1.GetHash());
288+
hasher.write(tx2.GetHash());
289+
assert(hasher.GetHash() == tx3.GetHash());
290+
// Verify that tx3 is 64 bytes in size (without witness).
291+
assert(GetSerializeSize(TX_NO_WITNESS(tx3)) == 64);
292+
}
293+
294+
CBlock block;
295+
block.vtx.push_back(MakeTransactionRef(tx1));
296+
block.vtx.push_back(MakeTransactionRef(tx2));
297+
uint256 merkle_root = block.hashMerkleRoot = BlockMerkleRoot(block);
298+
BOOST_CHECK(is_not_mutated(block, /*check_witness_root=*/false));
299+
300+
// Mutate the block by replacing the two transactions with one 64-byte
301+
// transaction that serializes into the concatenation of the txids of
302+
// the transactions in the unmutated block.
303+
block.vtx.clear();
304+
block.vtx.push_back(MakeTransactionRef(tx3));
305+
BOOST_CHECK(!block.vtx.back()->IsCoinBase());
306+
BOOST_CHECK(BlockMerkleRoot(block) == merkle_root);
307+
BOOST_CHECK(is_mutated(block, /*check_witness_root=*/false));
308+
}
309+
310+
{
311+
CBlock block;
312+
block.vtx.push_back(create_coinbase_tx(/*include_witness=*/true));
313+
{
314+
CMutableTransaction mtx;
315+
mtx.vin.resize(1);
316+
mtx.vin[0].scriptWitness.stack.resize(1);
317+
mtx.vin[0].scriptWitness.stack[0] = {0};
318+
block.vtx.push_back(MakeTransactionRef(mtx));
319+
}
320+
block.hashMerkleRoot = BlockMerkleRoot(block);
321+
// Block with witnesses is considered mutated if the witness commitment
322+
// is not validated.
323+
BOOST_CHECK(is_mutated(block, /*check_witness_root=*/false));
324+
// Block with invalid witness commitment is considered mutated.
325+
BOOST_CHECK(is_mutated(block, /*check_witness_root=*/true));
326+
327+
// Block with valid commitment is not mutated
328+
{
329+
auto commitment{BlockWitnessMerkleRoot(block)};
330+
insert_witness_commitment(block, commitment);
331+
block.hashMerkleRoot = BlockMerkleRoot(block);
332+
}
333+
BOOST_CHECK(is_not_mutated(block, /*check_witness_root=*/true));
334+
335+
// Malleating witnesses should be caught by `IsBlockMutated`.
336+
{
337+
CMutableTransaction mtx{*block.vtx[1]};
338+
assert(!mtx.vin[0].scriptWitness.stack[0].empty());
339+
++mtx.vin[0].scriptWitness.stack[0][0];
340+
block.vtx[1] = MakeTransactionRef(mtx);
341+
}
342+
// Without also updating the witness commitment, the merkle root should
343+
// not change when changing one of the witnesses.
344+
BOOST_CHECK(block.hashMerkleRoot == BlockMerkleRoot(block));
345+
BOOST_CHECK(is_mutated(block, /*check_witness_root=*/true));
346+
{
347+
auto commitment{BlockWitnessMerkleRoot(block)};
348+
insert_witness_commitment(block, commitment);
349+
block.hashMerkleRoot = BlockMerkleRoot(block);
350+
}
351+
BOOST_CHECK(is_not_mutated(block, /*check_witness_root=*/true));
352+
353+
// Test malleating the coinbase witness reserved value
354+
{
355+
CMutableTransaction mtx{*block.vtx[0]};
356+
mtx.vin[0].scriptWitness.stack.resize(0);
357+
block.vtx[0] = MakeTransactionRef(mtx);
358+
block.hashMerkleRoot = BlockMerkleRoot(block);
359+
}
360+
BOOST_CHECK(is_mutated(block, /*check_witness_root=*/true));
361+
}
362+
}
363+
148364
BOOST_AUTO_TEST_SUITE_END()

src/util/transaction_identifier.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class transaction_identifier
4444
constexpr void SetNull() { m_wrapped.SetNull(); }
4545
std::string GetHex() const { return m_wrapped.GetHex(); }
4646
std::string ToString() const { return m_wrapped.ToString(); }
47+
static constexpr auto size() { return decltype(m_wrapped)::size(); }
4748
constexpr const std::byte* data() const { return reinterpret_cast<const std::byte*>(m_wrapped.data()); }
4849
constexpr const std::byte* begin() const { return reinterpret_cast<const std::byte*>(m_wrapped.begin()); }
4950
constexpr const std::byte* end() const { return reinterpret_cast<const std::byte*>(m_wrapped.end()); }

0 commit comments

Comments
 (0)