From 169184ff674a57490077eff604e2888a055685e6 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Wed, 17 May 2023 17:19:49 +0200 Subject: [PATCH] test: add functional test for local tx relay --- test/functional/p2p_local_tx_relay.py | 248 ++++++++++++++++++++++++++ test/functional/test_runner.py | 1 + 2 files changed, 249 insertions(+) create mode 100755 test/functional/p2p_local_tx_relay.py diff --git a/test/functional/p2p_local_tx_relay.py b/test/functional/p2p_local_tx_relay.py new file mode 100755 index 00000000000000..0b115352df4699 --- /dev/null +++ b/test/functional/p2p_local_tx_relay.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017-2023 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +""" +Test how locally submitted transactions are sent to the network when sensitive relay is used. + +The topology in the test is: + +Bitcoin Core (node0) + ^ v The default full-outbound + block-only connections + | | (MAX_OUTBOUND_FULL_RELAY_CONNECTIONS + MAX_BLOCK_RELAY_ONLY_CONNECTIONS) + | | + | *-----> SOCKS5 Proxy ---> P2PInterface (self.socks5_server.conf.destinations[0]["node"]) + | | + | *-----> SOCKS5 Proxy ---> P2PInterface (self.socks5_server.conf.destinations[1]["node"]) + | ... + | | The sensitive relay TX recipients (SENSITIVE_RELAY_NUM_BROADCAST_PER_TX): + | | + | *-----> SOCKS5 Proxy ---> P2PInterface (self.socks5_server.conf.destinations[i]["node"]) + | | + | *-----> SOCKS5 Proxy ---> P2PInterface (self.socks5_server.conf.destinations[i + 1]["node"]) + | ... + | + *---------< observer_inbound +""" + +from test_framework.p2p import P2PDataStore, P2PInterface, P2P_SERVICES +from test_framework.messages import msg_getdata, msg_inv, msg_mempool, CInv, MSG_WTX +from test_framework.socks5 import Socks5Configuration, Socks5Server +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, MAX_NODES, p2p_port +from test_framework.wallet import MiniWallet + +MAX_OUTBOUND_FULL_RELAY_CONNECTIONS = 8 +MAX_BLOCK_RELAY_ONLY_CONNECTIONS = 2 +SENSITIVE_RELAY_NUM_BROADCAST_PER_TX = 5 + +g_p2p_index = None +def new_p2p_index(): + global g_p2p_index + g_p2p_index += 1 + return g_p2p_index + +# Fill addrman with these addresses. Must have enough Tor addresses, so that even +# if all 10 default connections are opened to a Tor address (!?) there must be more +# for sensitive relays. +addresses = [ + "1.65.195.98", + "2.59.236.56", + "2.83.114.20", + "2.248.194.16", + "5.2.154.6", + "5.101.140.30", + "5.128.87.126", + "5.144.21.49", + "5.172.132.104", + "5.188.62.18", + "5.200.2.180", + "8.129.184.255", + "8.209.105.138", + "12.34.98.148", + "14.199.102.151", + "18.27.79.17", + "18.27.124.231", + "18.216.249.151", + "23.88.155.58", + "23.93.101.158", + "[2001:19f0:1000:1db3:5400:4ff:fe56:5a8d]", + "[2001:19f0:5:24da:3eec:efff:feb9:f36e]", + "[2001:19f0:5:24da::]", + "[2001:19f0:5:4535:3eec:efff:feb9:87e4]", + "[2001:19f0:5:4535::]", + "[2001:1bc0:c1::2000]", + "[2001:1c04:4008:6300:8a5f:2678:114b:a660]", + "[2001:41d0:203:3739::]", + "[2001:41d0:203:8f49::]", + "[2001:41d0:203:bb0a::]", + "[2001:41d0:2:bf8f::]", + "[2001:41d0:303:de8b::]", + "[2001:41d0:403:3d61::]", + "[2001:41d0:405:9600::]", + "[2001:41d0:8:ed7f::1]", + "[2001:41d0:a:69a2::1]", + "[2001:41f0::62:6974:636f:696e]", + "[2001:470:1b62::]", + "[2001:470:1f05:43b:2831:8530:7179:5864]", + "[2001:470:1f09:b14::11]", + "2bqghnldu6mcug4pikzprwhtjjnsyederctvci6klcwzepnjd46ikjyd.onion", + "4lr3w2iyyl5u5l6tosizclykf5v3smqroqdn2i4h3kq6pfbbjb2xytad.onion", + "5g72ppm3krkorsfopcm2bi7wlv4ohhs4u4mlseymasn7g7zhdcyjpfid.onion", + "5sbmcl4m5api5tqafi4gcckrn3y52sz5mskxf3t6iw4bp7erwiptrgqd.onion", + "776aegl7tfhg6oiqqy76jnwrwbvcytsx2qegcgh2mjqujll4376ohlid.onion", + "77mdte42srl42shdh2mhtjr7nf7dmedqrw6bkcdekhdvmnld6ojyyiad.onion", + "azbpsh4arqlm6442wfimy7qr65bmha2zhgjg7wbaji6vvaug53hur2qd.onion", + "b64xcbleqmwgq2u46bh4hegnlrzzvxntyzbmucn3zt7cssm7y4ubv3id.onion", + "bsqbtcparrfihlwolt4xgjbf4cgqckvrvsfyvy6vhiqrnh4w6ghixoid.onion", + "bsqbtctulf2g4jtjsdfgl2ed7qs6zz5wqx27qnyiik7laockryvszqqd.onion", + "cwi3ekrwhig47dhhzfenr5hbvckj7fzaojygvazi2lucsenwbzwoyiqd.onion", + "devinbtcmwkuitvxl3tfi5of4zau46ymeannkjv6fpnylkgf3q5fa3id.onion", + "devinbtctu7uctl7hly2juu3thbgeivfnvw3ckj3phy6nyvpnx66yeyd.onion", + "devinbtcyk643iruzfpaxw3on2jket7rbjmwygm42dmdyub3ietrbmid.onion", + "dtql5vci4iaml4anmueftqr7bfgzqlauzfy4rc2tfgulldd3ekyijjyd.onion", + "emzybtc25oddoa2prol2znpz2axnrg6k77xwgirmhv7igoiucddsxiad.onion", + "emzybtc3ewh7zihpkdvuwlgxrhzcxy2p5fvjggp7ngjbxcytxvt4rjid.onion", + "emzybtc454ewbviqnmgtgx3rgublsgkk23r4onbhidcv36wremue4kqd.onion", + "emzybtc5bnpb2o6gh54oquiox54o4r7yn4a2wiiwzrjonlouaibm2zid.onion", + "fpz6r5ppsakkwypjcglz6gcnwt7ytfhxskkfhzu62tnylcknh3eq6pad.onion", + "255fhcp6ajvftnyo7bwz3an3t4a4brhopm3bamyh2iu5r3gnr2rq.b32.i2p", + "27yrtht5b5bzom2w5ajb27najuqvuydtzb7bavlak25wkufec5mq.b32.i2p", + "3gocb7wc4zvbmmebktet7gujccuux4ifk3kqilnxnj5wpdpqx2hq.b32.i2p", + "4fcc23wt3hyjk3csfzcdyjz5pcwg5dzhdqgma6bch2qyiakcbboa.b32.i2p", + "4osyqeknhx5qf3a73jeimexwclmt42cju6xdp7icja4ixxguu2hq.b32.i2p", + "4umsi4nlmgyp4rckosg4vegd2ysljvid47zu7pqsollkaszcbpqq.b32.i2p", + "6j2ezegd3e2e2x3o3pox335f5vxfthrrigkdrbgfbdjchm5h4awa.b32.i2p", + "6n36ljyr55szci5ygidmxqer64qr24f4qmnymnbvgehz7qinxnla.b32.i2p", + "72yjs6mvlby3ky6mgpvvlemmwq5pfcznrzd34jkhclgrishqdxva.b32.i2p", + "a5qsnv3maw77mlmmzlcglu6twje6ttctd3fhpbfwcbpmewx6fczq.b32.i2p", + "aovep2pco7v2k4rheofrgytbgk23eg22dczpsjqgqtxcqqvmxk6a.b32.i2p", + "bitcoi656nll5hu6u7ddzrmzysdtwtnzcnrjd4rfdqbeey7dmn5a.b32.i2p", + "brifkruhlkgrj65hffybrjrjqcgdgqs2r7siizb5b2232nruik3a.b32.i2p", + "c4gfnttsuwqomiygupdqqqyy5y5emnk5c73hrfvatri67prd7vyq.b32.i2p", + "day3hgxyrtwjslt54sikevbhxxs4qzo7d6vi72ipmscqtq3qmijq.b32.i2p", + "du5kydummi23bjfp6bd7owsvrijgt7zhvxmz5h5f5spcioeoetwq.b32.i2p", + "e55k6wu46rzp4pg5pk5npgbr3zz45bc3ihtzu2xcye5vwnzdy7pq.b32.i2p", + "eciohu5nq7vsvwjjc52epskuk75d24iccgzmhbzrwonw6lx4gdva.b32.i2p", + "ejlnngarmhqvune74ko7kk55xtgbz5i5ncs4vmnvjpy3l7y63xaa.b32.i2p", + "fhzlp3xroabohnmjonu5iqazwhlbbwh5cpujvw2azcu3srqdceja.b32.i2p", + "[fc32:17ea:e415:c3bf:9808:149d:b5a2:c9aa]", + "[fcc7:be49:ccd1:dc91:3125:f0da:457d:8ce]", + "[fcdc:73ae:b1a9:1bf8:d4c2:811:a4c7:c34e]", +] + +class P2PLocalTxRelay(BitcoinTestFramework): + def set_test_params(self): + self.disable_autoconnect = False + self.num_nodes = 1 + global g_p2p_index + g_p2p_index = self.num_nodes - 1 + + def setup_nodes(self): + # Start a SOCKS5 proxy server. + socks5_server_config = Socks5Configuration() + socks5_server_config.addr = ("127.0.0.1", p2p_port(new_p2p_index())) + socks5_server_config.unauth = True + socks5_server_config.auth = True + + self.socks5_server = Socks5Server(socks5_server_config) + self.socks5_server.start() + + ports_base = p2p_port(MAX_NODES) + 15000 + + def listen_callback(addr, port): + # Instruct the SOCKS5 server to redirect a connection to this Python P2P listener. + self.socks5_server.conf.destinations.append({ + "listen_addr": addr, + "listen_port": port, + "node": None, + "requested_to_addr": None, + }) + for i in range(MAX_OUTBOUND_FULL_RELAY_CONNECTIONS + MAX_BLOCK_RELAY_ONLY_CONNECTIONS + SENSITIVE_RELAY_NUM_BROADCAST_PER_TX): + # Create a Python P2P listening node and add it to self.socks5_server.conf.destinations[] + listener = P2PInterface() + listener.peer_connect_helper(dstaddr="0.0.0.0", dstport=0, net=self.chain, timeout_factor=self.options.timeout_factor) + listener.peer_connect_send_version(services=P2P_SERVICES) + self.network_thread.listen(p2p=listener, callback=listen_callback, addr="127.0.0.1", port=ports_base + i) + self.wait_until(lambda: len(self.socks5_server.conf.destinations) == i + 1) + self.socks5_server.conf.destinations[i]["node"] = listener + + self.add_nodes(self.num_nodes, extra_args=[ + [ + "-peerbloomfilters", # needed to test replies to MEMPOOL + "-sensitiverelayowntx", + f"-proxy={socks5_server_config.addr[0]}:{socks5_server_config.addr[1]}" + ] + ]) + self.start_nodes() + + def run_test(self): + node0 = self.nodes[0] + + # Fill node0's addrman. + for addr in addresses: + res = node0.addpeeraddress(address=addr, port=8333, tried=False) + if res["success"]: + self.log.debug(f"Added {addr} to node0's addrman") + else: + self.log.debug(f"Could not add {addr} to node0's addrman (collision?)") + + observer_inbound = node0.add_p2p_connection(P2PDataStore()) + + self.log.info("Getting out of IBD") + self.generatetoaddress(node0, 1, node0.get_deterministic_priv_key().address) + + num_initial_connections = MAX_OUTBOUND_FULL_RELAY_CONNECTIONS + MAX_BLOCK_RELAY_ONLY_CONNECTIONS + self.wait_until(lambda: self.socks5_server.conf.destinations_used == num_initial_connections) + assert_equal(self.socks5_server.conf.destinations_used, num_initial_connections) + + # The next opened connections should be "sensitive relay" for sending the transaction. + + miniwallet = MiniWallet(node0) + tx = miniwallet.send_self_transfer(from_node=node0) + self.log.info(f"Created transaction txid={tx['txid']}") + + for i in range(num_initial_connections, num_initial_connections + SENSITIVE_RELAY_NUM_BROADCAST_PER_TX): + tx_recipient = self.socks5_server.conf.destinations[i]["node"] + tx_recipient.wait_for_connect() + assert self.socks5_server.conf.destinations[i]["requested_to_addr"].endswith(".onion") + tx_recipient.wait_for_tx(tx['txid']) + tx_recipient.wait_for_disconnect() + assert_equal(tx_recipient.message_count, { + "version": 1, + "verack": 1, + "tx": 1, + "ping": 1 + }) + + wtxid_int = int(tx["wtxid"], 16) + inv = CInv(MSG_WTX, wtxid_int) + + observer_outbound = self.socks5_server.conf.destinations[0]["node"] + + self.log.info("Checking that the node hides the transaction from GETDATA requests") + for observer in [observer_inbound, observer_outbound]: + observer.last_message.pop("notfound", None) + observer.send_message(msg_getdata([inv])) + observer.wait_until(lambda: "notfound" in observer.last_message) + assert "tx" not in observer.last_message + + self.log.info("Checking that the node hides the transaction from MEMPOOL requests") + msg = f"Omitting INV for unbroadcast transaction (txid={tx['txid']}) from MEMPOOL reply".encode("utf-8") + for observer in [observer_inbound, observer_outbound]: + with node0.wait_for_debug_log([msg]): + observer.send_message(msg_mempool()) + assert "tx" not in observer.last_message + + self.log.info("Sending INV from an observer and waiting for GETDATA from node") + observer_inbound.tx_store[wtxid_int] = tx["tx"] + msg = f"Received own transaction (txid={tx['txid']}) from the network".encode("utf-8") + with node0.wait_for_debug_log(expected_msgs=[msg]): + observer_inbound.send_message(msg_inv([inv])) + + self.log.info("Waiting for normal broadcast to another observer") + observer_outbound.wait_for_inv([inv]) + + +if __name__ == '__main__': + P2PLocalTxRelay().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 5895e1de8453d5..38875f72436fda 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -302,6 +302,7 @@ 'rpc_dumptxoutset.py', 'feature_minchainwork.py', 'rpc_estimatefee.py', + 'p2p_local_tx_relay.py', 'rpc_getblockstats.py', 'feature_bind_port_externalip.py', 'wallet_create_tx.py --legacy-wallet',