feature_versionbits_warning.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2016-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  """Test version bits warning system.
   6  
   7  Generate chains with block versions that appear to be signalling unknown
   8  soft-forks, and test that warning alerts are generated.
   9  """
  10  import os
  11  import re
  12  
  13  from test_framework.blocktools import create_block, create_coinbase
  14  from test_framework.messages import msg_block
  15  from test_framework.p2p import P2PInterface
  16  from test_framework.test_framework import LimenkaTestFramework
  17  
  18  VB_PERIOD = 144           # versionbits period length for regtest
  19  VB_THRESHOLD = 108        # versionbits activation threshold for regtest
  20  VB_TOP_BITS = 0x20000000
  21  VB_UNKNOWN_BIT = 12       # Choose a bit unassigned to any deployment
  22  VB_UNKNOWN_VERSION = VB_TOP_BITS | (1 << VB_UNKNOWN_BIT)
  23  VB_BIP320_BIT = 13
  24  VB_BIP320_VERSION = VB_TOP_BITS | (1 << VB_BIP320_BIT)
  25  VB_BIP320_THRESHOLD = 76
  26  UNKNOWN_VERSION_SCHEMA = 0x60000000
  27  UNKNOWN_VERSION_SCHEMA_THRESHOLD = 51
  28  
  29  WARN_UNKNOWN_RULES_MINED = "Warning: Unrecognised block version (0x%08x) is being mined! Unknown rules may or may not be in effect" % (UNKNOWN_VERSION_SCHEMA,)
  30  WARN_UNKNOWN_BIT_MINED = f"Warning: Miners are attempting to activate unknown new rules (bit {VB_UNKNOWN_BIT})"
  31  # NOTE: WARN_BIP320_BIT_MINED includes VB_UNKNOWN_BIT because it persists from the earlier check
  32  WARN_BIP320_BIT_MINED = f"Warning: Miners are attempting to activate unknown new rules (bit {VB_UNKNOWN_BIT}, {VB_BIP320_BIT})"
  33  WARN_UNKNOWN_RULES_ACTIVE = f"Unknown new rules activated (versionbit {VB_UNKNOWN_BIT})"
  34  WARN_BIP320_BLOCK = "Miner violated version bit protocol"
  35  VB_PATTERN = re.compile("Unknown new rules activated.*versionbit")
  36  
  37  class VersionBitsWarningTest(LimenkaTestFramework):
  38      def set_test_params(self):
  39          self.setup_clean_chain = True
  40          self.num_nodes = 1
  41  
  42      def setup_network(self):
  43          self.alert_filename = os.path.join(self.options.tmpdir, "alert.txt")
  44          # Open and close to create zero-length file
  45          with open(self.alert_filename, 'w', encoding='utf8'):
  46              pass
  47          self.extra_args = [[f"-alertnotify=echo %s >> \"{self.alert_filename}\""]]
  48          self.setup_nodes()
  49  
  50      def send_blocks_with_version(self, peer, numblocks, version):
  51          """Send numblocks blocks to peer with version set"""
  52          tip = self.nodes[0].getbestblockhash()
  53          height = self.nodes[0].getblockcount()
  54          block_time = self.nodes[0].getblockheader(tip)["time"] + 1
  55          tip = int(tip, 16)
  56  
  57          for _ in range(numblocks):
  58              block = create_block(tip, create_coinbase(height + 1), block_time, version=version)
  59              block.solve()
  60              peer.send_message(msg_block(block))
  61              block_time += 1
  62              height += 1
  63              tip = block.sha256
  64          peer.sync_with_ping()
  65  
  66      def versionbits_in_alert_file(self):
  67          """Test that the versionbits warning has been written to the alert file."""
  68          with open(self.alert_filename, 'r', encoding='utf8') as f:
  69              alert_text = f.read()
  70          return VB_PATTERN.search(alert_text) is not None
  71  
  72      def run_test(self):
  73          node = self.nodes[0]
  74          peer = node.add_p2p_connection(P2PInterface())
  75  
  76          node_deterministic_address = node.get_deterministic_priv_key().address
  77          # Mine one period worth of blocks
  78          self.generatetoaddress(node, VB_PERIOD, node_deterministic_address)
  79  
  80          self.log.info("Check that there is no warning if previous VB_BLOCKS have <VB_THRESHOLD blocks with unknown versionbits version.")
  81          # Build one period of blocks with < VB_THRESHOLD blocks signaling some unknown bit
  82          self.send_blocks_with_version(peer, VB_THRESHOLD - 1, VB_UNKNOWN_VERSION)
  83          self.generatetoaddress(node, VB_PERIOD - VB_THRESHOLD + 1, node_deterministic_address)
  84  
  85          # Check that we're not getting any versionbit-related errors in get*info()
  86          assert not VB_PATTERN.match(",".join(node.getmininginfo()["warnings"]))
  87          assert not VB_PATTERN.match(",".join(node.getnetworkinfo()["warnings"]))
  88  
  89          self.log.info("Check that there is a warning if >50 blocks in the last 100 were an unknown version schema")
  90          # Build UNKNOWN_VERSION_SCHEMA_THRESHOLD blocks signaling some unknown schema
  91          self.send_blocks_with_version(peer, UNKNOWN_VERSION_SCHEMA_THRESHOLD, UNKNOWN_VERSION_SCHEMA)
  92          # Check that get*info() shows the 51/100 unknown block version warning
  93          assert(WARN_UNKNOWN_RULES_MINED in ",".join(node.getmininginfo()["warnings"]))
  94          assert(WARN_UNKNOWN_RULES_MINED in ",".join(node.getnetworkinfo()["warnings"]))
  95          # Close the period normally
  96          self.generatetoaddress(node, VB_PERIOD - UNKNOWN_VERSION_SCHEMA_THRESHOLD, node_deterministic_address)
  97          # Make sure the warning remains
  98          assert(WARN_UNKNOWN_RULES_MINED in ",".join(node.getmininginfo()["warnings"]))
  99          assert(WARN_UNKNOWN_RULES_MINED in ",".join(node.getnetworkinfo()["warnings"]))
 100  
 101          # Stop-start the node, and make sure the warning is gone
 102          self.restart_node(0)
 103          assert(WARN_UNKNOWN_RULES_MINED not in ",".join(node.getmininginfo()["warnings"]))
 104          assert(WARN_UNKNOWN_RULES_MINED not in ",".join(node.getnetworkinfo()["warnings"]))
 105          peer = node.add_p2p_connection(P2PInterface())
 106  
 107          self.log.info("Check that there is a warning if >50 blocks in the last 100 were an unknown version")
 108          # Build one period of blocks with VB_THRESHOLD blocks signaling some unknown bit
 109          self.send_blocks_with_version(peer, VB_THRESHOLD, VB_UNKNOWN_VERSION)
 110          self.generatetoaddress(node, VB_PERIOD - VB_THRESHOLD, node_deterministic_address)
 111  
 112          # Check that get*info() shows the 51/100 unknown block version warning
 113          assert(WARN_UNKNOWN_BIT_MINED in ",".join(node.getmininginfo()["warnings"]))
 114          assert(WARN_UNKNOWN_BIT_MINED in ",".join(node.getnetworkinfo()["warnings"]))
 115  
 116          self.log.info("Check that there is a warning if BIP320 is used, and a second persistent warning if >75 blocks in the last 100 were a BIP320 version")
 117          with node.busy_wait_for_debug_log([WARN_BIP320_BLOCK.encode('ascii')]):
 118              self.send_blocks_with_version(peer, VB_BIP320_THRESHOLD - 1, VB_BIP320_VERSION)
 119          # Check that get*info() doesn't shows the 76/100 unknown block version warning yet.
 120          assert(WARN_BIP320_BIT_MINED not in ",".join(node.getmininginfo()["warnings"]))
 121          assert(WARN_BIP320_BIT_MINED not in ",".join(node.getnetworkinfo()["warnings"]))
 122          # ...and it shouldn't show the BIP320-specific warning
 123          assert(WARN_BIP320_BLOCK not in node.getmininginfo()["warnings"])
 124          assert(WARN_BIP320_BLOCK not in node.getnetworkinfo()["warnings"])
 125          with node.busy_wait_for_debug_log([WARN_BIP320_BLOCK.encode('ascii'), b'Enqueuing UpdatedBlockTip']):
 126              self.send_blocks_with_version(peer, 1, VB_BIP320_VERSION)
 127          # Check that get*info() shows the 76/100 unknown block version warning.
 128          assert(WARN_BIP320_BIT_MINED in ",".join(node.getmininginfo()["warnings"]))
 129          assert(WARN_BIP320_BIT_MINED in ",".join(node.getnetworkinfo()["warnings"]))
 130          assert(WARN_BIP320_BLOCK not in node.getmininginfo()["warnings"])
 131          assert(WARN_BIP320_BLOCK not in node.getnetworkinfo()["warnings"])
 132          with node.busy_wait_for_debug_log([b'Enqueuing UpdatedBlockTip'], forbid_msgs=[WARN_BIP320_BLOCK.encode('ascii')]):
 133              self.generatetoaddress(node, 1, node_deterministic_address)
 134          # Only the 76/100 should persist
 135          assert(WARN_BIP320_BIT_MINED in ",".join(node.getmininginfo()["warnings"]))
 136          assert(WARN_BIP320_BIT_MINED in ",".join(node.getnetworkinfo()["warnings"]))
 137          assert(WARN_BIP320_BLOCK not in node.getmininginfo()["warnings"])
 138          assert(WARN_BIP320_BLOCK not in node.getnetworkinfo()["warnings"])
 139          self.generatetoaddress(node, VB_PERIOD - VB_BIP320_THRESHOLD - 1, node_deterministic_address)
 140  
 141          self.log.info("Check that there is a warning if previous VB_BLOCKS have >=VB_THRESHOLD blocks with unknown versionbits version.")
 142          # Mine a period worth of expected blocks so the generic block-version warning
 143          # is cleared. This will move the versionbit state to ACTIVE.
 144          self.generatetoaddress(node, VB_PERIOD, node_deterministic_address)
 145  
 146          # Stop-start the node. This is required because limenkad will only warn once about unknown versions or unknown rules activating.
 147          self.restart_node(0)
 148  
 149          # Generating one block guarantees that we'll get out of IBD
 150          self.generatetoaddress(node, 1, node_deterministic_address)
 151          self.wait_until(lambda: not node.getblockchaininfo()['initialblockdownload'])
 152          # Generating one more block will be enough to generate an error.
 153          self.generatetoaddress(node, 1, node_deterministic_address)
 154          # Check that get*info() shows the versionbits unknown rules warning
 155          assert WARN_UNKNOWN_RULES_ACTIVE in ",".join(node.getmininginfo()["warnings"])
 156          assert WARN_UNKNOWN_RULES_ACTIVE in ",".join(node.getnetworkinfo()["warnings"])
 157          # Check that the alert file shows the versionbits unknown rules warning
 158          self.wait_until(lambda: self.versionbits_in_alert_file())
 159  
 160  if __name__ == '__main__':
 161      VersionBitsWarningTest(__file__).main()
 162