#!/usr/bin/env python3 # Copyright (c) 2026 The Limenka developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Mempool bridge: cross-network transaction gossip between nodes. Node A runs the mempool bridge configured to connect to node B over the regtest network. The nodes share one regtest chain (B syncs from A once, then the regular P2P link is severed), so the bridge is the only gossip channel left. Two directions are proven: - A -> B: a wallet transaction on A appears in B's mempool through the bridge announcement. - B -> A: a signed transaction submitted only to B appears in A's mempool through the bridge receive path. Regtest is used (rather than the fork standalone chain) so the test exercises the bridge in isolation: two fork-standalone nodes cannot yet relay blocks cleanly between each other (a separate pre-existing issue), which starves the standard tx-relay machinery this test relies on. """ from test_framework.test_framework import LimenkaTestFramework from test_framework.util import assert_greater_than, p2p_port class MempoolBridgeTest(LimenkaTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.extra_args = [[ '-mempoolbridge', f'-bridgepeer=regtest:127.0.0.1:{p2p_port(1)}', ], [ ]] self.rpc_timeout = 240 def skip_test_if_missing_module(self): self.skip_if_no_wallet() def add_options(self, parser): self.add_wallet_options(parser, descriptors=True, legacy=False) def setup_network(self): # Do NOT auto-connect the nodes: the bridge must be the only gossip # channel between them. Chain sync is done manually via one-shot # addnode, then severed. self.setup_nodes() def run_test(self): node_a, node_b = self.nodes[0], self.nodes[1] # A mines mature coins; B stays at genesis for now. addr = node_a.getnewaddress() self.generatetoaddress(node_a, 110, addr, sync_fun=self.no_op) assert_greater_than(node_a.getbalance(), 100) # One-shot chain sync B <- A, then sever the regular P2P link so # the bridge is the only gossip channel between them. node_a.addnode(f"127.0.0.1:{p2p_port(1)}", "onetry") self.wait_until( lambda: node_b.getblockcount() == node_a.getblockcount(), timeout=60, ) assert not node_b.getblockchaininfo()['initialblockdownload'] node_a.disconnectnode("127.0.0.1") # The bridge peer appears on B (B accepted its inbound connection), # while A's connman is left with no other peers. self.wait_until( lambda: any(p.get('subver', '') == '/limenka-bridge:1.0.0/' for p in node_b.getpeerinfo()), timeout=60, ) self.wait_until(lambda: len(node_a.getpeerinfo()) == 0, timeout=60) # B -> A first: build and sign a transaction with A's wallet from a # fresh coinbase (nothing spent yet), submit it ONLY to B, and # verify A's mempool receives it through the bridge. unspent = node_a.listunspent() assert_greater_than(len(unspent), 0) utxo = unspent[0] psbt = node_a.walletcreatefundedpsbt( [{"txid": utxo["txid"], "vout": utxo["vout"]}], {addr: "1"}, 0, {"subtractFeeFromOutputs": [0]})["psbt"] processed = node_a.walletprocesspsbt(psbt) assert processed["complete"], processed signed_hex = node_a.finalizepsbt(processed["psbt"])["hex"] txid_b2a = node_b.sendrawtransaction(signed_hex) self.log.info(f"B -> A txid: {txid_b2a}") self.wait_until( lambda: txid_b2a in node_a.getrawmempool(), timeout=60, ) # A -> B: a wallet transaction on A must reach B's mempool via the # bridge announcement (A's wallet avoids the now-mempool-spent # coinbase from the previous step). txid_a2b = node_a.sendtoaddress(addr, "3") self.log.info(f"A -> B txid: {txid_a2b}") self.wait_until( lambda: txid_a2b in node_b.getrawmempool(), timeout=60, ) self.log.info("bridge relay complete: mempools are one in both directions") if __name__ == '__main__': MempoolBridgeTest(__file__).main()