Skip to content

Commit 5bf4f5b

Browse files
committed
[test] Add regression test for #27608
1 parent 49257c0 commit 5bf4f5b

File tree

2 files changed

+97
-0
lines changed

2 files changed

+97
-0
lines changed

test/functional/p2p_mutated_blocks.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
"""
7+
Test that an attacker can't degrade compact block relay by sending unsolicited
8+
mutated blocks to clear in-flight blocktxn requests from other honest peers.
9+
"""
10+
11+
from test_framework.p2p import P2PInterface
12+
from test_framework.messages import (
13+
BlockTransactions,
14+
msg_cmpctblock,
15+
msg_block,
16+
msg_blocktxn,
17+
HeaderAndShortIDs,
18+
)
19+
from test_framework.test_framework import BitcoinTestFramework
20+
from test_framework.blocktools import (
21+
COINBASE_MATURITY,
22+
create_block,
23+
add_witness_commitment,
24+
NORMAL_GBT_REQUEST_PARAMS,
25+
)
26+
from test_framework.util import assert_equal
27+
from test_framework.wallet import MiniWallet
28+
import copy
29+
30+
class MutatedBlocksTest(BitcoinTestFramework):
31+
def set_test_params(self):
32+
self.setup_clean_chain = True
33+
self.num_nodes = 1
34+
35+
def run_test(self):
36+
self.wallet = MiniWallet(self.nodes[0])
37+
self.generate(self.wallet, COINBASE_MATURITY)
38+
39+
honest_relayer = self.nodes[0].add_outbound_p2p_connection(P2PInterface(), p2p_idx=0, connection_type="outbound-full-relay")
40+
attacker = self.nodes[0].add_p2p_connection(P2PInterface())
41+
42+
# Create new block with two transactions (coinbase + 1 self-transfer).
43+
# The self-transfer transaction is needed to trigger a compact block
44+
# `getblocktxn` roundtrip.
45+
tx = self.wallet.create_self_transfer()["tx"]
46+
block = create_block(tmpl=self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS), txlist=[tx])
47+
add_witness_commitment(block)
48+
block.solve()
49+
50+
# Create mutated version of the block by changing the transaction
51+
# version on the self-transfer.
52+
mutated_block = copy.deepcopy(block)
53+
mutated_block.vtx[1].nVersion = 4
54+
55+
# Announce the new block via a compact block through the honest relayer
56+
cmpctblock = HeaderAndShortIDs()
57+
cmpctblock.initialize_from_block(block, use_witness=True)
58+
honest_relayer.send_message(msg_cmpctblock(cmpctblock.to_p2p()))
59+
60+
# Wait for a `getblocktxn` that attempts to fetch the self-transfer
61+
def self_transfer_requested():
62+
if not honest_relayer.last_message.get('getblocktxn'):
63+
return False
64+
65+
get_block_txn = honest_relayer.last_message['getblocktxn']
66+
return get_block_txn.block_txn_request.blockhash == block.sha256 and \
67+
get_block_txn.block_txn_request.indexes == [1]
68+
honest_relayer.wait_until(self_transfer_requested, timeout=5)
69+
70+
# Block at height 101 should be the only one in flight from peer 0
71+
peer_info_prior_to_attack = self.nodes[0].getpeerinfo()
72+
assert_equal(peer_info_prior_to_attack[0]['id'], 0)
73+
assert_equal([101], peer_info_prior_to_attack[0]["inflight"])
74+
75+
# Attempt to clear the honest relayer's download request by sending the
76+
# mutated block (as the attacker).
77+
with self.nodes[0].assert_debug_log(expected_msgs=["bad-txnmrklroot, hashMerkleRoot mismatch"]):
78+
attacker.send_message(msg_block(mutated_block))
79+
# Attacker should get disconnected for sending a mutated block
80+
attacker.wait_for_disconnect(timeout=5)
81+
82+
# Block at height 101 should *still* be the only block in-flight from
83+
# peer 0
84+
peer_info_after_attack = self.nodes[0].getpeerinfo()
85+
assert_equal(peer_info_after_attack[0]['id'], 0)
86+
assert_equal([101], peer_info_after_attack[0]["inflight"])
87+
88+
# The honest relayer should be able to complete relaying the block by
89+
# sending the blocktxn that was requested.
90+
block_txn = msg_blocktxn()
91+
block_txn.block_transactions = BlockTransactions(blockhash=block.sha256, transactions=[tx])
92+
honest_relayer.send_and_ping(block_txn)
93+
assert_equal(self.nodes[0].getbestblockhash(), block.hash)
94+
95+
if __name__ == '__main__':
96+
MutatedBlocksTest().main()

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@
306306
'wallet_crosschain.py',
307307
'mining_basic.py',
308308
'feature_signet.py',
309+
'p2p_mutated_blocks.py',
309310
'wallet_implicitsegwit.py --legacy-wallet',
310311
'rpc_named_arguments.py',
311312
'feature_startupnotify.py',

0 commit comments

Comments
 (0)