/ test / functional / p2p_blockfilters.py
p2p_blockfilters.py
  1  #!/usr/bin/env python3
  2  # Copyright (c) 2019-2022 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  """Tests NODE_COMPACT_FILTERS (BIP 157/158).
  6  
  7  Tests that a node configured with -blockfilterindex and -peerblockfilters signals
  8  NODE_COMPACT_FILTERS and can serve cfilters, cfheaders and cfcheckpts.
  9  """
 10  
 11  from test_framework.messages import (
 12      FILTER_TYPE_BASIC,
 13      NODE_COMPACT_FILTERS,
 14      hash256,
 15      msg_getcfcheckpt,
 16      msg_getcfheaders,
 17      msg_getcfilters,
 18      ser_uint256,
 19      uint256_from_str,
 20  )
 21  from test_framework.p2p import P2PInterface
 22  from test_framework.test_framework import BitcoinTestFramework
 23  from test_framework.util import (
 24      assert_equal,
 25  )
 26  
 27  class FiltersClient(P2PInterface):
 28      def __init__(self):
 29          super().__init__()
 30          # Store the cfilters received.
 31          self.cfilters = []
 32  
 33      def pop_cfilters(self):
 34          cfilters = self.cfilters
 35          self.cfilters = []
 36          return cfilters
 37  
 38      def on_cfilter(self, message):
 39          """Store cfilters received in a list."""
 40          self.cfilters.append(message)
 41  
 42  
 43  class CompactFiltersTest(BitcoinTestFramework):
 44      def set_test_params(self):
 45          self.setup_clean_chain = True
 46          self.rpc_timeout = 480
 47          self.num_nodes = 2
 48          self.extra_args = [
 49              ["-blockfilterindex", "-peerblockfilters"],
 50              ["-blockfilterindex"],
 51          ]
 52  
 53      def run_test(self):
 54          # Node 0 supports COMPACT_FILTERS, node 1 does not.
 55          peer_0 = self.nodes[0].add_p2p_connection(FiltersClient())
 56          peer_1 = self.nodes[1].add_p2p_connection(FiltersClient())
 57  
 58          # Nodes 0 & 1 share the same first 999 blocks in the chain.
 59          self.generate(self.nodes[0], 999)
 60  
 61          # Stale blocks by disconnecting nodes 0 & 1, mining, then reconnecting
 62          self.disconnect_nodes(0, 1)
 63  
 64          stale_block_hash = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[0]
 65          self.nodes[0].syncwithvalidationinterfacequeue()
 66          assert_equal(self.nodes[0].getblockcount(), 1000)
 67  
 68          self.generate(self.nodes[1], 1001, sync_fun=self.no_op)
 69          assert_equal(self.nodes[1].getblockcount(), 2000)
 70  
 71          # Check that nodes have signalled NODE_COMPACT_FILTERS correctly.
 72          assert peer_0.nServices & NODE_COMPACT_FILTERS != 0
 73          assert peer_1.nServices & NODE_COMPACT_FILTERS == 0
 74  
 75          # Check that the localservices is as expected.
 76          assert int(self.nodes[0].getnetworkinfo()['localservices'], 16) & NODE_COMPACT_FILTERS != 0
 77          assert int(self.nodes[1].getnetworkinfo()['localservices'], 16) & NODE_COMPACT_FILTERS == 0
 78  
 79          self.log.info("get cfcheckpt on chain to be re-orged out.")
 80          request = msg_getcfcheckpt(
 81              filter_type=FILTER_TYPE_BASIC,
 82              stop_hash=int(stale_block_hash, 16),
 83          )
 84          peer_0.send_and_ping(message=request)
 85          response = peer_0.last_message['cfcheckpt']
 86          assert_equal(response.filter_type, request.filter_type)
 87          assert_equal(response.stop_hash, request.stop_hash)
 88          assert_equal(len(response.headers), 1)
 89  
 90          self.log.info("Reorg node 0 to a new chain.")
 91          self.connect_nodes(0, 1)
 92          self.sync_blocks(timeout=600)
 93          self.nodes[0].syncwithvalidationinterfacequeue()
 94  
 95          main_block_hash = self.nodes[0].getblockhash(1000)
 96          assert main_block_hash != stale_block_hash, "node 0 chain did not reorganize"
 97  
 98          self.log.info("Check that peers can fetch cfcheckpt on active chain.")
 99          tip_hash = self.nodes[0].getbestblockhash()
100          request = msg_getcfcheckpt(
101              filter_type=FILTER_TYPE_BASIC,
102              stop_hash=int(tip_hash, 16),
103          )
104          peer_0.send_and_ping(request)
105          response = peer_0.last_message['cfcheckpt']
106          assert_equal(response.filter_type, request.filter_type)
107          assert_equal(response.stop_hash, request.stop_hash)
108  
109          main_cfcheckpt = self.nodes[0].getblockfilter(main_block_hash, 'basic')['header']
110          tip_cfcheckpt = self.nodes[0].getblockfilter(tip_hash, 'basic')['header']
111          assert_equal(
112              response.headers,
113              [int(header, 16) for header in (main_cfcheckpt, tip_cfcheckpt)],
114          )
115  
116          self.log.info("Check that peers can fetch cfcheckpt on stale chain.")
117          request = msg_getcfcheckpt(
118              filter_type=FILTER_TYPE_BASIC,
119              stop_hash=int(stale_block_hash, 16),
120          )
121          peer_0.send_and_ping(request)
122          response = peer_0.last_message['cfcheckpt']
123  
124          stale_cfcheckpt = self.nodes[0].getblockfilter(stale_block_hash, 'basic')['header']
125          assert_equal(
126              response.headers,
127              [int(header, 16) for header in (stale_cfcheckpt, )],
128          )
129  
130          self.log.info("Check that peers can fetch cfheaders on active chain.")
131          request = msg_getcfheaders(
132              filter_type=FILTER_TYPE_BASIC,
133              start_height=1,
134              stop_hash=int(main_block_hash, 16),
135          )
136          peer_0.send_and_ping(request)
137          response = peer_0.last_message['cfheaders']
138          main_cfhashes = response.hashes
139          assert_equal(len(main_cfhashes), 1000)
140          assert_equal(
141              compute_last_header(response.prev_header, response.hashes),
142              int(main_cfcheckpt, 16),
143          )
144  
145          self.log.info("Check that peers can fetch cfheaders on stale chain.")
146          request = msg_getcfheaders(
147              filter_type=FILTER_TYPE_BASIC,
148              start_height=1,
149              stop_hash=int(stale_block_hash, 16),
150          )
151          peer_0.send_and_ping(request)
152          response = peer_0.last_message['cfheaders']
153          stale_cfhashes = response.hashes
154          assert_equal(len(stale_cfhashes), 1000)
155          assert_equal(
156              compute_last_header(response.prev_header, response.hashes),
157              int(stale_cfcheckpt, 16),
158          )
159  
160          self.log.info("Check that peers can fetch cfilters.")
161          stop_hash = self.nodes[0].getblockhash(10)
162          request = msg_getcfilters(
163              filter_type=FILTER_TYPE_BASIC,
164              start_height=1,
165              stop_hash=int(stop_hash, 16),
166          )
167          peer_0.send_and_ping(request)
168          response = peer_0.pop_cfilters()
169          assert_equal(len(response), 10)
170  
171          self.log.info("Check that cfilter responses are correct.")
172          for cfilter, cfhash, height in zip(response, main_cfhashes, range(1, 11)):
173              block_hash = self.nodes[0].getblockhash(height)
174              assert_equal(cfilter.filter_type, FILTER_TYPE_BASIC)
175              assert_equal(cfilter.block_hash, int(block_hash, 16))
176              computed_cfhash = uint256_from_str(hash256(cfilter.filter_data))
177              assert_equal(computed_cfhash, cfhash)
178  
179          self.log.info("Check that peers can fetch cfilters for stale blocks.")
180          request = msg_getcfilters(
181              filter_type=FILTER_TYPE_BASIC,
182              start_height=1000,
183              stop_hash=int(stale_block_hash, 16),
184          )
185          peer_0.send_and_ping(request)
186          response = peer_0.pop_cfilters()
187          assert_equal(len(response), 1)
188  
189          cfilter = response[0]
190          assert_equal(cfilter.filter_type, FILTER_TYPE_BASIC)
191          assert_equal(cfilter.block_hash, int(stale_block_hash, 16))
192          computed_cfhash = uint256_from_str(hash256(cfilter.filter_data))
193          assert_equal(computed_cfhash, stale_cfhashes[999])
194  
195          self.log.info("Requests to node 1 without NODE_COMPACT_FILTERS results in disconnection.")
196          requests = [
197              msg_getcfcheckpt(
198                  filter_type=FILTER_TYPE_BASIC,
199                  stop_hash=int(main_block_hash, 16),
200              ),
201              msg_getcfheaders(
202                  filter_type=FILTER_TYPE_BASIC,
203                  start_height=1000,
204                  stop_hash=int(main_block_hash, 16),
205              ),
206              msg_getcfilters(
207                  filter_type=FILTER_TYPE_BASIC,
208                  start_height=1000,
209                  stop_hash=int(main_block_hash, 16),
210              ),
211          ]
212          for request in requests:
213              peer_1 = self.nodes[1].add_p2p_connection(P2PInterface())
214              with self.nodes[1].assert_debug_log(expected_msgs=["requested unsupported block filter type"]):
215                  peer_1.send_message(request)
216                  peer_1.wait_for_disconnect()
217  
218          self.log.info("Check that invalid requests result in disconnection.")
219          requests = [
220              # Requesting too many filters results in disconnection.
221              (
222                  msg_getcfilters(
223                      filter_type=FILTER_TYPE_BASIC,
224                      start_height=0,
225                      stop_hash=int(main_block_hash, 16),
226                  ), "requested too many cfilters/cfheaders"
227              ),
228              # Requesting too many filter headers results in disconnection.
229              (
230                  msg_getcfheaders(
231                      filter_type=FILTER_TYPE_BASIC,
232                      start_height=0,
233                      stop_hash=int(tip_hash, 16),
234                  ), "requested too many cfilters/cfheaders"
235              ),
236              # Requesting unknown filter type results in disconnection.
237              (
238                  msg_getcfcheckpt(
239                      filter_type=255,
240                      stop_hash=int(main_block_hash, 16),
241                  ), "requested unsupported block filter type"
242              ),
243              # Requesting unknown hash results in disconnection.
244              (
245                  msg_getcfcheckpt(
246                      filter_type=FILTER_TYPE_BASIC,
247                      stop_hash=123456789,
248                  ), "requested invalid block hash"
249              ),
250              (
251                  # Request with (start block height > stop block height) results in disconnection.
252                  msg_getcfheaders(
253                      filter_type=FILTER_TYPE_BASIC,
254                      start_height=1000,
255                      stop_hash=int(self.nodes[0].getblockhash(999), 16),
256                  ), "sent invalid getcfilters/getcfheaders with start height 1000 and stop height 999"
257              ),
258          ]
259          for request, expected_log_msg in requests:
260              peer_0 = self.nodes[0].add_p2p_connection(P2PInterface())
261              with self.nodes[0].assert_debug_log(expected_msgs=[expected_log_msg]):
262                  peer_0.send_message(request)
263                  peer_0.wait_for_disconnect()
264  
265          self.log.info("Test -peerblockfilters without -blockfilterindex raises an error")
266          self.stop_node(0)
267          self.nodes[0].extra_args = ["-peerblockfilters"]
268          msg = "Error: Cannot set -peerblockfilters without -blockfilterindex."
269          self.nodes[0].assert_start_raises_init_error(expected_msg=msg)
270  
271          self.log.info("Test unknown value to -blockfilterindex raises an error")
272          self.nodes[0].extra_args = ["-blockfilterindex=abc"]
273          msg = "Error: Unknown -blockfilterindex value abc."
274          self.nodes[0].assert_start_raises_init_error(expected_msg=msg)
275  
276  def compute_last_header(prev_header, hashes):
277      """Compute the last filter header from a starting header and a sequence of filter hashes."""
278      header = ser_uint256(prev_header)
279      for filter_hash in hashes:
280          header = hash256(ser_uint256(filter_hash) + header)
281      return uint256_from_str(header)
282  
283  
284  if __name__ == '__main__':
285      CompactFiltersTest().main()