p2p_blockfilters.py raw

   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_equal(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_equal(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()
 287