mempool_bridge_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  """Mempool bridge: cross-network transaction gossip between nodes.
   6  
   7  Node A runs the mempool bridge configured to connect to node B over the
   8  regtest network.  The nodes share one regtest chain (B syncs from A once,
   9  then the regular P2P link is severed), so the bridge is the only gossip
  10  channel left.  Two directions are proven:
  11  
  12    - A -> B: a wallet transaction on A appears in B's mempool through the
  13      bridge announcement.
  14    - B -> A: a signed transaction submitted only to B appears in A's
  15      mempool through the bridge receive path.
  16  
  17  Regtest is used (rather than the fork standalone chain) so the test
  18  exercises the bridge in isolation: two fork-standalone nodes cannot yet
  19  relay blocks cleanly between each other (a separate pre-existing issue),
  20  which starves the standard tx-relay machinery this test relies on.
  21  """
  22  from test_framework.test_framework import LimenkaTestFramework
  23  from test_framework.util import assert_greater_than, p2p_port
  24  
  25  
  26  class MempoolBridgeTest(LimenkaTestFramework):
  27      def set_test_params(self):
  28          self.setup_clean_chain = True
  29          self.num_nodes = 2
  30          self.extra_args = [[
  31              '-mempoolbridge',
  32              f'-bridgepeer=regtest:127.0.0.1:{p2p_port(1)}',
  33          ], [
  34          ]]
  35          self.rpc_timeout = 240
  36  
  37      def skip_test_if_missing_module(self):
  38          self.skip_if_no_wallet()
  39  
  40      def add_options(self, parser):
  41          self.add_wallet_options(parser, descriptors=True, legacy=False)
  42  
  43      def setup_network(self):
  44          # Do NOT auto-connect the nodes: the bridge must be the only gossip
  45          # channel between them.  Chain sync is done manually via one-shot
  46          # addnode, then severed.
  47          self.setup_nodes()
  48  
  49      def run_test(self):
  50          node_a, node_b = self.nodes[0], self.nodes[1]
  51  
  52          # A mines mature coins; B stays at genesis for now.
  53          addr = node_a.getnewaddress()
  54          self.generatetoaddress(node_a, 110, addr, sync_fun=self.no_op)
  55          assert_greater_than(node_a.getbalance(), 100)
  56  
  57          # One-shot chain sync B <- A, then sever the regular P2P link so
  58          # the bridge is the only gossip channel between them.
  59          node_a.addnode(f"127.0.0.1:{p2p_port(1)}", "onetry")
  60          self.wait_until(
  61              lambda: node_b.getblockcount() == node_a.getblockcount(),
  62              timeout=60,
  63          )
  64          assert not node_b.getblockchaininfo()['initialblockdownload']
  65          node_a.disconnectnode("127.0.0.1")
  66          # The bridge peer appears on B (B accepted its inbound connection),
  67          # while A's connman is left with no other peers.
  68          self.wait_until(
  69              lambda: any(p.get('subver', '') == '/limenka-bridge:1.0.0/' for p in node_b.getpeerinfo()),
  70              timeout=60,
  71          )
  72          self.wait_until(lambda: len(node_a.getpeerinfo()) == 0, timeout=60)
  73  
  74          # B -> A first: build and sign a transaction with A's wallet from a
  75          # fresh coinbase (nothing spent yet), submit it ONLY to B, and
  76          # verify A's mempool receives it through the bridge.
  77          unspent = node_a.listunspent()
  78          assert_greater_than(len(unspent), 0)
  79          utxo = unspent[0]
  80          psbt = node_a.walletcreatefundedpsbt(
  81              [{"txid": utxo["txid"], "vout": utxo["vout"]}],
  82              {addr: "1"}, 0, {"subtractFeeFromOutputs": [0]})["psbt"]
  83          processed = node_a.walletprocesspsbt(psbt)
  84          assert processed["complete"], processed
  85          signed_hex = node_a.finalizepsbt(processed["psbt"])["hex"]
  86          txid_b2a = node_b.sendrawtransaction(signed_hex)
  87          self.log.info(f"B -> A txid: {txid_b2a}")
  88          self.wait_until(
  89              lambda: txid_b2a in node_a.getrawmempool(),
  90              timeout=60,
  91          )
  92  
  93          # A -> B: a wallet transaction on A must reach B's mempool via the
  94          # bridge announcement (A's wallet avoids the now-mempool-spent
  95          # coinbase from the previous step).
  96          txid_a2b = node_a.sendtoaddress(addr, "3")
  97          self.log.info(f"A -> B txid: {txid_a2b}")
  98          self.wait_until(
  99              lambda: txid_a2b in node_b.getrawmempool(),
 100              timeout=60,
 101          )
 102  
 103          self.log.info("bridge relay complete: mempools are one in both directions")
 104  
 105  
 106  if __name__ == '__main__':
 107      MempoolBridgeTest(__file__).main()
 108