p2p_bip434_feature.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 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 BIP434 feature negotiation."""
   7  
   8  from test_framework.messages import (
   9      msg_version,
  10      ser_compact_size,
  11  )
  12  from test_framework.p2p import (
  13      P2PInterface,
  14      P2P_SERVICES,
  15      P2P_SUBVERSION,
  16      P2P_VERSION_RELAY,
  17  )
  18  from test_framework.test_framework import BitcoinTestFramework
  19  from test_framework.util import assert_equal
  20  
  21  # Pre-BIP434 protocol version
  22  PRE_FEATURE_VERSION = 70016
  23  # Protocol version which enables BIP434 FEATURE negotiation
  24  FEATURE_VERSION = 70017
  25  # BIP434 wire-format limits
  26  MIN_FEATUREID_LENGTH = 4
  27  MAX_FEATUREID_LENGTH = 80
  28  MAX_FEATUREDATA_LENGTH = 512
  29  
  30  
  31  class RawFeature:
  32      """A FEATURE message with a hand-crafted payload."""
  33      msgtype = b"feature"
  34  
  35      def __init__(self, payload):
  36          self.payload = payload
  37  
  38      def serialize(self):
  39          return self.payload
  40  
  41      def __repr__(self):
  42          return f"RawFeature(payload_len={len(self.payload)})"
  43  
  44  
  45  def feature_wire(feature_id, feature_data, *, trailing=b""):
  46      """Build a FEATURE payload plus optional trailing bytes."""
  47      if isinstance(feature_id, str):
  48          feature_id = feature_id.encode()
  49      return (ser_compact_size(len(feature_id)) + feature_id
  50              + ser_compact_size(len(feature_data)) + feature_data
  51              + trailing)
  52  
  53  
  54  def _version_msg(nversion):
  55      v = msg_version()
  56      v.nVersion = nversion
  57      v.strSubVer = P2P_SUBVERSION
  58      v.nServices = P2P_SERVICES
  59      v.relay = P2P_VERSION_RELAY
  60      return v
  61  
  62  
  63  class FeaturePeer(P2PInterface):
  64      """P2PInterface that counts FEATURE messages received."""
  65  
  66      def __init__(self):
  67          super().__init__()
  68          self.got_feature_count = 0
  69          self.last_feature = None
  70  
  71      def on_feature(self, message):
  72          self.got_feature_count += 1
  73          self.last_feature = message
  74  
  75  
  76  class FeaturePeerNoVerack(FeaturePeer):
  77      """Peer that records but does not auto-reply to the node's VERSION, so the
  78      node stays in the post-VERSION / pre-VERACK window where FEATURE messages
  79      are valid to send."""
  80  
  81      def on_version(self, message):
  82          self.nServices = message.nServices
  83          self.relay = message.relay
  84  
  85  
  86  class P2PBIP434FeatureTest(BitcoinTestFramework):
  87      def set_test_params(self):
  88          self.num_nodes = 1
  89          # peertimeout=999 prevents the node from kicking the peer for being idle
  90          self.extra_args = [["-debug=net", "-peertimeout=999"]]
  91  
  92      def run_test(self):
  93          self.test_advertised_version()
  94          self.test_no_feature_to_pre_70017_peer()
  95          self.test_features_announced_to_modern_peer()
  96          self.test_feature_after_verack_disconnects()
  97          self.test_feature_before_version_ignored()
  98          self.test_feature_id_length_boundaries()
  99          self.test_feature_data_length_boundaries()
 100          self.test_trailing_bytes_disconnect()
 101          self.test_truncated_feature_id_disconnect()
 102          self.test_truncated_feature_data_disconnect()
 103          self.test_non_ascii_feature_id_accepted()
 104          self.test_many_features_in_handshake()
 105          self.test_recv_feature_from_pre_70017_peer()
 106  
 107      def _silent_peer(self, *, nversion=FEATURE_VERSION):
 108          peer = self.nodes[0].add_p2p_connection(
 109              FeaturePeerNoVerack(),
 110              send_version=False, wait_for_verack=False,
 111          )
 112          peer.send_without_ping(_version_msg(nversion))
 113          peer.wait_for_verack()
 114          return peer
 115  
 116      def _expect_accept(self, payload, *, log_substring="unknown feature advertised",
 117                        nversion=FEATURE_VERSION):
 118          peer = self._silent_peer(nversion=nversion)
 119          with self.nodes[0].assert_debug_log([log_substring], timeout=2):
 120              peer.send_without_ping(RawFeature(payload))
 121          assert peer.is_connected, "peer disconnected after a well-formed FEATURE"
 122          self.nodes[0].disconnect_p2ps()
 123  
 124      def _expect_disconnect(self, payload, *, log_substring="invalid feature payload",
 125                            nversion=FEATURE_VERSION):
 126          peer = self._silent_peer(nversion=nversion)
 127          with self.nodes[0].assert_debug_log([log_substring], timeout=2):
 128              peer.send_without_ping(RawFeature(payload))
 129              peer.wait_for_disconnect()
 130  
 131      def test_advertised_version(self):
 132          self.log.info("Test that node advertises FEATURE to peer with protocol version 70017")
 133          peer = self.nodes[0].add_p2p_connection(FeaturePeer())
 134          assert_equal(peer.last_message["version"].nVersion, FEATURE_VERSION)
 135          self.nodes[0].disconnect_p2ps()
 136  
 137      def test_no_feature_to_pre_70017_peer(self):
 138          self.log.info("Test that node doesn't send FEATURE to a peer with protocol version <70017")
 139          peer = self.nodes[0].add_p2p_connection(
 140              FeaturePeer(), send_version=False, wait_for_verack=False,
 141          )
 142          peer.send_without_ping(_version_msg(PRE_FEATURE_VERSION))
 143          peer.wait_for_verack()
 144          # The node's full handshake has now been delivered to us; if any
 145          # FEATURE would have been sent it would be in last_message by now.
 146          assert_equal(peer.got_feature_count, 0)
 147          self.nodes[0].disconnect_p2ps()
 148  
 149      def test_features_announced_to_modern_peer(self):
 150          self.log.info("Test that node announces correct number of features to a 70017 peer")
 151          peer = self.nodes[0].add_p2p_connection(FeaturePeer())
 152          assert_equal(peer.got_feature_count, 0)
 153          self.nodes[0].disconnect_p2ps()
 154  
 155      def test_feature_after_verack_disconnects(self):
 156          self.log.info("Test that FEATURE after VERACK triggers disconnect")
 157          peer = self.nodes[0].add_p2p_connection(FeaturePeer())
 158          peer.sync_with_ping()  # ensure node has set fSuccessfullyConnected
 159          with self.nodes[0].assert_debug_log(["feature received after verack"], timeout=2):
 160              peer.send_without_ping(RawFeature(feature_wire(b"abcd", b"")))
 161              peer.wait_for_disconnect()
 162  
 163      def test_feature_before_version_ignored(self):
 164          self.log.info("Test that FEATURE before any VERSION is silently ignored")
 165          peer = self.nodes[0].add_p2p_connection(
 166              FeaturePeer(), send_version=False, wait_for_verack=False,
 167          )
 168          with self.nodes[0].assert_debug_log(
 169              ["non-version message before version handshake"], timeout=2,
 170          ):
 171              peer.send_without_ping(RawFeature(feature_wire(b"abcd", b"")))
 172          assert peer.is_connected
 173          self.nodes[0].disconnect_p2ps()
 174  
 175      def test_feature_id_length_boundaries(self):
 176          self.log.info("Test feature_id length boundaries")
 177          for length, accept in [(0, False),
 178                                 (3, False),
 179                                 (MIN_FEATUREID_LENGTH, True),
 180                                 (MAX_FEATUREID_LENGTH, True),
 181                                 (MAX_FEATUREID_LENGTH + 1, False)]:
 182              payload = feature_wire(b"a" * length, b"")
 183              if accept:
 184                  self._expect_accept(payload)
 185              else:
 186                  self._expect_disconnect(payload)
 187  
 188      def test_feature_data_length_boundaries(self):
 189          self.log.info("Test feature_data length boundaries")
 190          for length, accept in [(0, True),
 191                                 (MAX_FEATUREDATA_LENGTH, True),
 192                                 (MAX_FEATUREDATA_LENGTH + 1, False)]:
 193              payload = feature_wire(b"abcd", b"\x00" * length)
 194              if accept:
 195                  self._expect_accept(payload)
 196              else:
 197                  self._expect_disconnect(payload)
 198  
 199      def test_trailing_bytes_disconnect(self):
 200          self.log.info("Test that trailing bytes after data triggers disconnect")
 201          self._expect_disconnect(
 202              feature_wire(b"abcd", b"", trailing=b"\x00"),
 203          )
 204  
 205      def test_truncated_feature_id_disconnect(self):
 206          self.log.info("Test that truncated feature_id triggers disconnect")
 207          payload = ser_compact_size(10) + b"abcde"
 208          self._expect_disconnect(payload)
 209  
 210      def test_truncated_feature_data_disconnect(self):
 211          self.log.info("Test that truncated feature_data triggers disconnect")
 212          payload = (ser_compact_size(MIN_FEATUREID_LENGTH) + b"abcd"
 213                     + ser_compact_size(10) + b"xx")
 214          self._expect_disconnect(payload)
 215  
 216      def test_non_ascii_feature_id_accepted(self):
 217          self.log.info("Test that feature_id with non-ASCII bytes is still accepted")
 218          # BIP says SHOULD, not MUST, on this
 219          self._expect_accept(feature_wire(b"\x00\xff\x01\x7f", b""))
 220  
 221      def test_many_features_in_handshake(self):
 222          self.log.info("Test multiple FEATURE advertisements")
 223          peer = self._silent_peer()
 224          with self.nodes[0].assert_debug_log(["unknown feature advertised"], timeout=2):
 225              for i in range(16):
 226                  peer.send_without_ping(
 227                      RawFeature(feature_wire(f"feat{i:04d}".encode(), b""))
 228                  )
 229          assert peer.is_connected
 230          self.nodes[0].disconnect_p2ps()
 231  
 232      def test_recv_feature_from_pre_70017_peer(self):
 233          self.log.info("Test that FEATURE from <70017 peer triggers disconnect")
 234          self._expect_disconnect(
 235              feature_wire(b"abcd", b""),
 236              log_substring="feature received with incompatible version",
 237              nversion=PRE_FEATURE_VERSION,
 238          )
 239  
 240  
 241  if __name__ == "__main__":
 242      P2PBIP434FeatureTest(__file__).main()
 243