fork_prefork_sync_test.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2026 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  """Pre-fork block sync through the mempool bridge.
   6  
   7  The fork chain has its own P2P magic, so a fresh fork node cannot
   8  download the pre-fork (mainnet) history from spamchain peers through
   9  normal net_processing.  While the fork tip is below the activation MTP,
  10  the mempool bridge's foreign peers serve that history: headers are
  11  validated with ProcessNewBlockHeaders and blocks with ProcessNewBlock.
  12  
  13  This test keeps the fork inactive (activation far in the future), lets
  14  node A mine a mainnet-style chain, and verifies node B - whose ONLY link
  15  to A is the bridge - syncs the whole chain through it.
  16  """
  17  import hashlib
  18  
  19  from test_framework.address import byte_to_base58, hash160
  20  from test_framework.descriptors import descsum_create
  21  from test_framework.key import ECKey
  22  from test_framework.segwit_addr import encode_segwit_address
  23  from test_framework.test_framework import LimenkaTestFramework
  24  from test_framework.util import p2p_port
  25  
  26  GENESIS_TIME = 1231006505
  27  # Far-future activation MTP: the fork never activates in this test, so all
  28  # blocks are pre-fork (mainnet-rule) blocks.
  29  ACTIVATION_FAR = 1999999999
  30  
  31  
  32  class ForkPreforkSyncTest(LimenkaTestFramework):
  33      def set_test_params(self):
  34          self.setup_clean_chain = True
  35          self.num_nodes = 2
  36          self.chain = 'limenka'
  37          self.extra_args = [[
  38              '-forkactivationtime=%d' % ACTIVATION_FAR,
  39              '-forkdelaysteps=1024',
  40              '-forkmineondemand',
  41              '-forkstandalone',
  42              '-fallbackfee=0.0002',
  43              '-addresstype=bech32',
  44          ], [
  45              '-forkactivationtime=%d' % ACTIVATION_FAR,
  46              '-forkdelaysteps=1024',
  47              '-forkmineondemand',
  48              '-forkstandalone',
  49              '-fallbackfee=0.0002',
  50              '-addresstype=bech32',
  51              '-mempoolbridge',
  52              # Node B's bridge connects to node A (fork network).
  53              f'-bridgepeer=fork:127.0.0.1:{p2p_port(0)}',
  54          ]]
  55          self.rpc_timeout = 240
  56  
  57      def skip_test_if_missing_module(self):
  58          self.skip_if_no_wallet()
  59  
  60      def add_options(self, parser):
  61          self.add_wallet_options(parser, descriptors=True, legacy=False)
  62  
  63      def init_wallet(self, *, node):
  64          wallet_name = self.default_wallet_name if self.wallet_names is None else self.wallet_names[node] if node < len(self.wallet_names) else False
  65          if wallet_name is not False:
  66              n = self.nodes[node]
  67              if wallet_name is not None:
  68                  n.createwallet(wallet_name=wallet_name, descriptors=True, load_on_startup=True)
  69                  if node == 0:
  70                      seed = hashlib.sha256(b'prefork-sync-funding').digest()
  71                      key = ECKey()
  72                      key.set(seed, compressed=True)
  73                      priv = key.get_bytes()
  74                      wif = byte_to_base58(priv + b'\x01', 128)
  75                      desc = descsum_create(f"wpkh({wif})")
  76                      import_res = n.importdescriptors([{
  77                          "desc": desc,
  78                          "timestamp": GENESIS_TIME + 600,
  79                          "active": False,
  80                          "internal": False,
  81                      }])
  82                      assert all(r['success'] for r in import_res)
  83                      self.funding_key = priv
  84                      self.funding_pub = key.get_pubkey().get_bytes()
  85  
  86      def setup_network(self):
  87          # Do NOT auto-connect the nodes: the bridge must be the only link
  88          # between A and B.
  89          self.setup_nodes()
  90  
  91      def run_test(self):
  92          node_a, node_b = self.nodes[0], self.nodes[1]
  93  
  94          # A mines a mainnet-style (pre-fork) chain to a fixed bech32 addr.
  95          p2wpkh_program = hash160(self.funding_pub)
  96          addr = encode_segwit_address("bf", 0, p2wpkh_program)
  97          self.generatetoaddress(node_a, 100, addr, sync_fun=self.no_op)
  98          assert node_a.getblockcount() == 100
  99  
 100          # B's bridge connects to A and downloads the whole chain.
 101          self.wait_until(
 102              lambda: node_b.getblockcount() == 100,
 103              timeout=240,
 104          )
 105          info_b = node_b.getblockchaininfo()
 106          assert info_b['blocks'] == 100, info_b
 107          assert info_b['headers'] == 100, info_b
 108  
 109          # The blocks are consensus-valid on B (same chain hashes).
 110          assert node_b.getblockhash(99) == node_a.getblockhash(99)
 111  
 112          self.log.info("prefork block sync complete: B synced A's chain via the bridge")
 113  
 114  
 115  if __name__ == '__main__':
 116      ForkPreforkSyncTest(__file__).main()
 117