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