1 #!/usr/bin/env python3
2 # Copyright (c) 2020-present 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 Test BIP 37
7 """
8 9 from test_framework.messages import (
10 CInv,
11 COIN,
12 MAX_BLOOM_FILTER_SIZE,
13 MAX_BLOOM_HASH_FUNCS,
14 MSG_WTX,
15 MSG_BLOCK,
16 MSG_FILTERED_BLOCK,
17 msg_filteradd,
18 msg_filterclear,
19 msg_filterload,
20 msg_getdata,
21 msg_mempool,
22 msg_version,
23 )
24 from test_framework.p2p import (
25 P2PInterface,
26 P2P_SERVICES,
27 P2P_SUBVERSION,
28 P2P_VERSION,
29 p2p_lock,
30 )
31 from test_framework.script import MAX_SCRIPT_ELEMENT_SIZE
32 from test_framework.test_framework import BitcoinTestFramework
33 from test_framework.wallet import (
34 MiniWallet,
35 getnewdestination,
36 )
37 38 39 class P2PBloomFilter(P2PInterface):
40 # This is a P2SH watch-only wallet
41 watch_script_pubkey = bytes.fromhex('a914ffffffffffffffffffffffffffffffffffffffff87')
42 # The initial filter (n=10, fp=0.000001) with just the above scriptPubKey added
43 watch_filter_init = msg_filterload(
44 data=
45 b'@\x00\x08\x00\x80\x00\x00 \x00\xc0\x00 \x04\x00\x08$\x00\x04\x80\x00\x00 \x00\x00\x00\x00\x80\x00\x00@\x00\x02@ \x00',
46 nHashFuncs=19,
47 nTweak=0,
48 nFlags=1,
49 )
50 51 def __init__(self):
52 super().__init__()
53 self._tx_received = False
54 self._merkleblock_received = False
55 56 def on_inv(self, message):
57 want = msg_getdata()
58 for i in message.inv:
59 # inv messages can only contain TX or BLOCK, so translate BLOCK to FILTERED_BLOCK
60 if i.type == MSG_BLOCK:
61 want.inv.append(CInv(MSG_FILTERED_BLOCK, i.hash))
62 else:
63 want.inv.append(i)
64 if len(want.inv):
65 self.send_without_ping(want)
66 67 def on_merkleblock(self, message):
68 self._merkleblock_received = True
69 70 def on_tx(self, message):
71 self._tx_received = True
72 73 @property
74 def tx_received(self):
75 with p2p_lock:
76 return self._tx_received
77 78 @tx_received.setter
79 def tx_received(self, value):
80 with p2p_lock:
81 self._tx_received = value
82 83 @property
84 def merkleblock_received(self):
85 with p2p_lock:
86 return self._merkleblock_received
87 88 @merkleblock_received.setter
89 def merkleblock_received(self, value):
90 with p2p_lock:
91 self._merkleblock_received = value
92 93 94 class FilterTest(BitcoinTestFramework):
95 def set_test_params(self):
96 self.num_nodes = 1
97 # whitelist peers to speed up tx relay / mempool sync
98 self.noban_tx_relay = True
99 self.extra_args = [[
100 '-peerbloomfilters',
101 ]]
102 103 def generatetoscriptpubkey(self, scriptpubkey):
104 """Helper to generate a single block to the given scriptPubKey."""
105 return self.generatetodescriptor(self.nodes[0], 1, f'raw({scriptpubkey.hex()})')[0]
106 107 def test_size_limits(self, filter_peer):
108 self.log.info('Check that too large filter is rejected')
109 with self.nodes[0].assert_debug_log(['Misbehaving']):
110 filter_peer.send_and_ping(msg_filterload(data=b'\xbb'*(MAX_BLOOM_FILTER_SIZE+1)))
111 112 self.log.info('Check that max size filter is accepted')
113 with self.nodes[0].assert_debug_log([], unexpected_msgs=['Misbehaving']):
114 filter_peer.send_and_ping(msg_filterload(data=b'\xbb'*(MAX_BLOOM_FILTER_SIZE)))
115 filter_peer.send_and_ping(msg_filterclear())
116 117 self.log.info('Check that filter with too many hash functions is rejected')
118 with self.nodes[0].assert_debug_log(['Misbehaving']):
119 filter_peer.send_and_ping(msg_filterload(data=b'\xaa', nHashFuncs=MAX_BLOOM_HASH_FUNCS+1))
120 121 self.log.info('Check that filter with max hash functions is accepted')
122 with self.nodes[0].assert_debug_log([], unexpected_msgs=['Misbehaving']):
123 filter_peer.send_and_ping(msg_filterload(data=b'\xaa', nHashFuncs=MAX_BLOOM_HASH_FUNCS))
124 # Don't send filterclear until next two filteradd checks are done
125 126 self.log.info('Check that max size data element to add to the filter is accepted')
127 with self.nodes[0].assert_debug_log([], unexpected_msgs=['Misbehaving']):
128 filter_peer.send_and_ping(msg_filteradd(data=b'\xcc'*(MAX_SCRIPT_ELEMENT_SIZE)))
129 130 self.log.info('Check that too large data element to add to the filter is rejected')
131 with self.nodes[0].assert_debug_log(['Misbehaving']):
132 filter_peer.send_and_ping(msg_filteradd(data=b'\xcc'*(MAX_SCRIPT_ELEMENT_SIZE+1)))
133 134 filter_peer.send_and_ping(msg_filterclear())
135 136 def test_msg_mempool(self):
137 self.log.info("Check that a node with bloom filters enabled services p2p mempool messages")
138 filter_peer = P2PBloomFilter()
139 140 self.log.info("Create two tx before connecting, one relevant to the node another that is not")
141 rel_txid = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=filter_peer.watch_script_pubkey, amount=1 * COIN)["txid"]
142 irr_result = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=getnewdestination()[1], amount=2 * COIN)
143 irr_txid = irr_result["txid"]
144 irr_wtxid = irr_result["wtxid"]
145 146 self.log.info("Send a mempool msg after connecting and check that the relevant tx is announced")
147 self.nodes[0].add_p2p_connection(filter_peer)
148 filter_peer.send_and_ping(filter_peer.watch_filter_init)
149 filter_peer.send_without_ping(msg_mempool())
150 filter_peer.wait_for_tx(rel_txid)
151 152 self.log.info("Request the irrelevant transaction even though it was not announced")
153 filter_peer.send_without_ping(msg_getdata([CInv(t=MSG_WTX, h=int(irr_wtxid, 16))]))
154 self.log.info("We should get it anyway because it was in the mempool on connection to peer")
155 filter_peer.wait_for_tx(irr_txid)
156 157 def test_frelay_false(self, filter_peer):
158 self.log.info("Check that a node with fRelay set to false does not receive invs until the filter is set")
159 filter_peer.tx_received = False
160 self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=filter_peer.watch_script_pubkey, amount=9 * COIN)
161 # Sync to make sure the reason filter_peer doesn't receive the tx is not p2p delays
162 filter_peer.sync_with_ping()
163 assert not filter_peer.tx_received
164 165 # Clear the mempool so that this transaction does not impact subsequent tests
166 self.generate(self.nodes[0], 1)
167 168 def test_filter(self, filter_peer):
169 # Set the bloomfilter using filterload
170 filter_peer.send_and_ping(filter_peer.watch_filter_init)
171 # If fRelay is not already True, sending filterload sets it to True
172 assert self.nodes[0].getpeerinfo()[0]['relaytxes']
173 174 self.log.info('Check that we receive merkleblock and tx if the filter matches a tx in a block')
175 block_hash = self.generatetoscriptpubkey(filter_peer.watch_script_pubkey)
176 txid = self.nodes[0].getblock(block_hash)['tx'][0]
177 filter_peer.wait_for_merkleblock(block_hash)
178 filter_peer.wait_for_tx(txid)
179 180 self.log.info('Check that we only receive a merkleblock if the filter does not match a tx in a block')
181 filter_peer.tx_received = False
182 block_hash = self.generatetoscriptpubkey(getnewdestination()[1])
183 filter_peer.wait_for_merkleblock(block_hash)
184 assert not filter_peer.tx_received
185 186 self.log.info('Check that we not receive a tx if the filter does not match a mempool tx')
187 filter_peer.merkleblock_received = False
188 filter_peer.tx_received = False
189 self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=getnewdestination()[1], amount=7 * COIN)
190 filter_peer.sync_with_ping()
191 assert not filter_peer.merkleblock_received
192 assert not filter_peer.tx_received
193 194 self.log.info('Check that we receive a tx if the filter matches a mempool tx')
195 filter_peer.merkleblock_received = False
196 txid = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=filter_peer.watch_script_pubkey, amount=9 * COIN)["txid"]
197 filter_peer.wait_for_tx(txid)
198 assert not filter_peer.merkleblock_received
199 200 self.log.info('Check that after deleting filter all txs get relayed again')
201 filter_peer.send_and_ping(msg_filterclear())
202 for _ in range(5):
203 txid = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=getnewdestination()[1], amount=7 * COIN)["txid"]
204 filter_peer.wait_for_tx(txid)
205 206 self.log.info('Check that request for filtered blocks is ignored if no filter is set')
207 filter_peer.merkleblock_received = False
208 filter_peer.tx_received = False
209 with self.nodes[0].assert_debug_log(expected_msgs=['received getdata']):
210 block_hash = self.generatetoscriptpubkey(getnewdestination()[1])
211 filter_peer.wait_for_inv([CInv(MSG_BLOCK, int(block_hash, 16))])
212 filter_peer.sync_with_ping()
213 assert not filter_peer.merkleblock_received
214 assert not filter_peer.tx_received
215 216 self.log.info('Check that sending "filteradd" if no filter is set is treated as misbehavior')
217 with self.nodes[0].assert_debug_log(['Misbehaving']):
218 filter_peer.send_and_ping(msg_filteradd(data=b'letsmisbehave'))
219 220 self.log.info("Check that division-by-zero remote crash bug [CVE-2013-5700] is fixed")
221 filter_peer.send_and_ping(msg_filterload(data=b'', nHashFuncs=1))
222 filter_peer.send_and_ping(msg_filteradd(data=b'letstrytocrashthisnode'))
223 self.nodes[0].disconnect_p2ps()
224 225 def run_test(self):
226 self.wallet = MiniWallet(self.nodes[0])
227 228 filter_peer = self.nodes[0].add_p2p_connection(P2PBloomFilter())
229 self.log.info('Test filter size limits')
230 self.test_size_limits(filter_peer)
231 232 self.log.info('Test BIP 37 for a node with fRelay = True (default)')
233 self.test_filter(filter_peer)
234 self.nodes[0].disconnect_p2ps()
235 236 self.log.info('Test BIP 37 for a node with fRelay = False')
237 # Add peer but do not send version yet
238 filter_peer_without_nrelay = self.nodes[0].add_p2p_connection(P2PBloomFilter(), send_version=False, wait_for_verack=False)
239 # Send version with relay=False
240 version_without_fRelay = msg_version()
241 version_without_fRelay.nVersion = P2P_VERSION
242 version_without_fRelay.strSubVer = P2P_SUBVERSION
243 version_without_fRelay.nServices = P2P_SERVICES
244 version_without_fRelay.relay = 0
245 filter_peer_without_nrelay.send_without_ping(version_without_fRelay)
246 filter_peer_without_nrelay.wait_for_verack()
247 assert not self.nodes[0].getpeerinfo()[0]['relaytxes']
248 self.test_frelay_false(filter_peer_without_nrelay)
249 self.test_filter(filter_peer_without_nrelay)
250 251 self.test_msg_mempool()
252 253 254 if __name__ == '__main__':
255 FilterTest(__file__).main()
256