|
| 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() |
0 commit comments