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