Skip to content

Commit d8087ad

Browse files
committed
[test] IsBlockMutated unit tests
1 parent 1ed2c98 commit d8087ad

File tree

1 file changed

+211
-0
lines changed

1 file changed

+211
-0
lines changed

src/test/validation_tests.cpp

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,215 @@ BOOST_AUTO_TEST_CASE(test_assumeutxo)
150150
BOOST_CHECK_EQUAL(out110_2.nChainTx, 111U);
151151
}
152152

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+
153364
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)