p2p_blockfilters.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2019-2022 The Limenka 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 LimenkaTestFramework
  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(LimenkaTestFramework):
  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(__file__).main()
 286