fork_parent_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  """Fork node pre-fork IBD from a spamchain parent peer over normal P2P.
   6  
   7  The fork chain has its own P2P magic, but a fork node may also hold
   8  connections to spamchain (parent chain) peers that speak the parent
   9  network's magic (-parentpeer).  While the fork tip is below the fork
  10  point those parent peers are ordinary download peers: headers via
  11  ProcessNewBlockHeaders, blocks via ProcessNewBlock.  This test keeps the
  12  fork inactive on both sides, lets node A (speaking mainnet magic via
  13  -p2pmagic) mine a mainnet-style chain, and verifies node B - connected to
  14  A only through -parentpeer - syncs the whole chain.
  15  """
  16  import hashlib
  17  
  18  from test_framework.address import byte_to_base58, hash160
  19  from test_framework.descriptors import descsum_create
  20  from test_framework.key import ECKey
  21  from test_framework.segwit_addr import encode_segwit_address
  22  from test_framework.test_framework import LimenkaTestFramework
  23  from test_framework.util import p2p_port
  24  
  25  GENESIS_TIME = 1231006505
  26  ACTIVATION_FAR = 1999999999
  27  MAINNET_MAGIC = 'f9beb4d9'
  28  
  29  
  30  class ForkParentSyncTest(LimenkaTestFramework):
  31      def set_test_params(self):
  32          self.setup_clean_chain = True
  33          self.num_nodes = 2
  34          self.chain = 'limenka'
  35          self.extra_args = [[
  36              '-forkactivationtime=%d' % ACTIVATION_FAR,
  37              '-forkdelaysteps=1024',
  38              '-forkmineondemand',
  39              '-forkstandalone',
  40              '-fallbackfee=0.0002',
  41              '-addresstype=bech32',
  42              # A impersonates a spamchain node (speaks mainnet magic).
  43              f'-p2pmagic={MAINNET_MAGIC}',
  44          ], [
  45              '-forkactivationtime=%d' % ACTIVATION_FAR,
  46              '-forkdelaysteps=1024',
  47              '-forkmineondemand',
  48              '-forkstandalone',
  49              '-fallbackfee=0.0002',
  50              '-addresstype=bech32',
  51              # B connects to A as a spamchain parent peer.
  52              f'-parentpeer=127.0.0.1:{p2p_port(0)}',
  53          ]]
  54          self.rpc_timeout = 240
  55  
  56      def skip_test_if_missing_module(self):
  57          self.skip_if_no_wallet()
  58  
  59      def add_options(self, parser):
  60          self.add_wallet_options(parser, descriptors=True, legacy=False)
  61  
  62      def init_wallet(self, *, node):
  63          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
  64          if wallet_name is not False:
  65              n = self.nodes[node]
  66              if wallet_name is not None and node == 0:
  67                  n.createwallet(wallet_name=wallet_name, descriptors=True, load_on_startup=True)
  68                  seed = hashlib.sha256(b'fork-parent-sync-funding').digest()
  69                  key = ECKey()
  70                  key.set(seed, compressed=True)
  71                  priv = key.get_bytes()
  72                  wif = byte_to_base58(priv + b'\x01', 128)
  73                  desc = descsum_create(f"wpkh({wif})")
  74                  import_res = n.importdescriptors([{
  75                      "desc": desc,
  76                      "timestamp": GENESIS_TIME + 600,
  77                      "active": False,
  78                      "internal": False,
  79                  }])
  80                  assert all(r['success'] for r in import_res)
  81                  self.funding_pub = key.get_pubkey().get_bytes()
  82  
  83      def setup_network(self):
  84          # No auto-connect: B's only link to A is the parent connection.
  85          self.setup_nodes()
  86  
  87      def run_test(self):
  88          node_a, node_b = self.nodes[0], self.nodes[1]
  89  
  90          # A mines a mainnet-style (pre-fork) chain to a fixed bech32 addr.
  91          p2wpkh_program = hash160(self.funding_pub)
  92          addr = encode_segwit_address("bf", 0, p2wpkh_program)
  93          self.generatetoaddress(node_a, 100, addr, sync_fun=self.no_op)
  94          assert node_a.getblockcount() == 100
  95  
  96          # B's parent peer downloads the whole chain through normal P2P.
  97          self.wait_until(
  98              lambda: node_b.getblockcount() == 100,
  99              timeout=240,
 100          )
 101          info_b = node_b.getblockchaininfo()
 102          assert info_b['blocks'] == 100, info_b
 103          assert node_b.getblockhash(99) == node_a.getblockhash(99)
 104  
 105          # The parent peer is connected and classified as a parent peer.
 106          peers = node_b.getpeerinfo()
 107          assert any(p['addr'] == f"127.0.0.1:{p2p_port(0)}" for p in peers), peers
 108  
 109          self.log.info("parent peer sync complete: B synced A's chain via -parentpeer")
 110  
 111  
 112  if __name__ == '__main__':
 113      ForkParentSyncTest(__file__).main()
 114