From e85cc59badd357219213a6bda985a9fd93910a5c 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 private broadcast --- test/functional/feature_config_args.py | 9 +- test/functional/p2p_private_broadcast.py | 308 +++++++++++++++++++++++ test/functional/test_runner.py | 1 + 3 files changed, 313 insertions(+), 5 deletions(-) create mode 100755 test/functional/p2p_private_broadcast.py diff --git a/test/functional/feature_config_args.py b/test/functional/feature_config_args.py index 8884ce0e3348c1..2bcaf0f0ad26cf 100755 --- a/test/functional/feature_config_args.py +++ b/test/functional/feature_config_args.py @@ -332,12 +332,11 @@ def test_privatebroadcast(self): "private broadcast needs to open new connections to randomly chosen " "Tor or I2P peers" : # -onion= makes the Tor network reachable - ["-privatebroadcast", "-walletbroadcast=0", "-connect=127.0.0.1:8333", "-onion=127.0.0.1:9050"], - - "The wallet currently does not support private transactions broadcast. " - "Set -walletbroadcast=0 or -privatebroadcast=0 to proceed.": - ["-privatebroadcast", "-walletbroadcast"] + ["-privatebroadcast", "-walletbroadcast=0", "-connect=127.0.0.1:8333", "-onion=127.0.0.1:9050"] } + if self.is_wallet_compiled(): + args_errors["The wallet currently does not support private transactions broadcast. " + "Set -walletbroadcast=0 or -privatebroadcast=0 to proceed."] = ["-privatebroadcast", "-walletbroadcast"] for msg, args in args_errors.items(): self.nodes[0].assert_start_raises_init_error(extra_args=args, expected_msg=f"Error: {msg}") diff --git a/test/functional/p2p_private_broadcast.py b/test/functional/p2p_private_broadcast.py new file mode 100755 index 00000000000000..75efc09ca32cf0 --- /dev/null +++ b/test/functional/p2p_private_broadcast.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017-present 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 private broadcast is used. + +The topology in the test is: + +Bitcoin Core (tx_originator) + ^ 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 private broadcast TX recipients + | | (NUM_PRIVATE_BROADCAST_PER_TX) + | | plus maybe some feeler or extra block only connections: + | | + | *-----> SOCKS5 Proxy ---> Bitcoin Core (node1) (self.socks5_server.conf.destinations[i]["node"] is None) + | | ^ + | | *----< P2PInterface (far_observer) (to check Bitcoin Core relays the tx) + | | + | *-----> SOCKS5 Proxy ---> P2PInterface (self.socks5_server.conf.destinations[i + 1]["node"]) + | ... + | + *---------< P2PDataStore (observer_inbound) +""" + +from test_framework.p2p import ( + P2PDataStore, + P2PInterface, + P2P_SERVICES, + P2P_VERSION, +) +from test_framework.messages import ( + CAddress, + CInv, + MSG_WTX, + msg_inv, +) +from test_framework.socks5 import ( + Socks5Configuration, + Socks5Server, +) +from test_framework.test_framework import ( + BitcoinTestFramework, +) +from test_framework.util import ( + MAX_NODES, + assert_equal, + p2p_port, +) +from test_framework.wallet import ( + MiniWallet, +) + +MAX_OUTBOUND_FULL_RELAY_CONNECTIONS = 8 +MAX_BLOCK_RELAY_ONLY_CONNECTIONS = 2 +NUM_INITIAL_CONNECTIONS = MAX_OUTBOUND_FULL_RELAY_CONNECTIONS + MAX_BLOCK_RELAY_ONLY_CONNECTIONS +NUM_PRIVATE_BROADCAST_PER_TX = 5 + +# 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 private broadcast. +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", + "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 P2PPrivateBroadcast(BitcoinTestFramework): + def set_test_params(self): + self.disable_autoconnect = False + self.num_nodes = 2 + + def setup_nodes(self): + # Start a SOCKS5 proxy server. + socks5_server_config = Socks5Configuration() + # self.nodes[0] listens on p2p_port(0), + # self.nodes[1] listens on p2p_port(1), + # thus we tell the SOCKS5 server to listen on p2p_port(self.num_nodes (which is 2)) + socks5_server_config.addr = ("127.0.0.1", p2p_port(self.num_nodes)) + 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 + + # Add 2 because one feeler and one extra block relay connections may + # sneak in between the private broadcast connections. + for i in range(NUM_INITIAL_CONNECTIONS + NUM_PRIVATE_BROADCAST_PER_TX + 2): + if i == NUM_INITIAL_CONNECTIONS: + # Instruct the SOCKS5 server to redirect the first private + # broadcast connection from nodes[0] to nodes[1] + self.socks5_server.conf.destinations.append({ + "to_addr": "127.0.0.1", # nodes[1] listen address + "to_port": p2p_port(1), # nodes[1] listen port + "node": None, + "requested_to_addr": None, + }) + else: + # 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, + # Instruct the SOCKS5 server to redirect a connection to this Python P2P listener. + callback=lambda addr, port: self.socks5_server.conf.destinations.append({ + "to_addr": addr, + "to_port": port, + "node": None, + "requested_to_addr": None, + }), + addr="127.0.0.1", + port=ports_base + i) + # Wait until the callback has been called and it has done append(). + self.wait_until(lambda: len(self.socks5_server.conf.destinations) == i + 1) + + self.socks5_server.conf.destinations[i]["node"] = listener + + self.extra_args = [ + [ + "-cjdnsreachable", # needed to be able to add CJDNS addresses to addrman (otherwise they are unroutable). + "-test=addrman", + "-privatebroadcast", + f"-proxy={socks5_server_config.addr[0]}:{socks5_server_config.addr[1]}", + ], + [ + "-connect=0", + f"-bind=127.0.0.1:{p2p_port(1)}=onion", # consider all incoming as coming from Tor + ], + ] + super().setup_nodes() + + def setup_network(self): + self.setup_nodes() + + def run_test(self): + tx_originator = self.nodes[0] + + observer_inbound = tx_originator.add_p2p_connection(P2PDataStore()) + + # Fill tx_originator's addrman. + for addr in addresses: + res = tx_originator.addpeeraddress(address=addr, port=8333, tried=False) + if not res["success"]: + self.log.debug(f"Could not add {addr} to tx_originator's addrman (collision?)") + + self.wait_until(lambda: self.socks5_server.conf.destinations_used == NUM_INITIAL_CONNECTIONS) + + node1 = self.nodes[1] + far_observer = node1.add_p2p_connection(P2PInterface()) + + # The next opened connections should be "private broadcast" for sending the transaction. + + miniwallet = MiniWallet(tx_originator) + tx = miniwallet.send_self_transfer(from_node=tx_originator) + self.log.info(f"Created transaction txid={tx['txid']}") + + self.log.debug(f"Inspecting outbound connection i={NUM_INITIAL_CONNECTIONS}, must be the first private broadcast connection") + self.wait_until(lambda: len(node1.getrawmempool()) > 0) + far_observer.wait_for_tx(tx["txid"]) + self.log.debug(f"Outbound connection i={NUM_INITIAL_CONNECTIONS}: the private broadcast target received and further relayed the transaction") + + num_private_broadcast_opened = 1 # already 1 connection to node1, confirmed by far_observer getting the tx + for i in range(NUM_INITIAL_CONNECTIONS + 1, len(self.socks5_server.conf.destinations)): + self.log.debug(f"Inspecting outbound connection i={i}") + # At this point the connection may not yet have been established (A), + # may be active (B), or may have already been closed (C). + peer = self.socks5_server.conf.destinations[i]["node"] + peer.wait_until(lambda: peer.message_count["version"] == 1, check_connected=False) + # Now it is either (B) or (C). + requested_to_addr = self.socks5_server.conf.destinations[i]["requested_to_addr"] + if peer.last_message["version"].nServices != 0: + self.log.debug(f"Outbound connection i={i} to {requested_to_addr} not a private broadcast, ignoring it (maybe feeler or extra block only)") + continue + self.log.debug(f"Outbound connection i={i} to {requested_to_addr} must be a private broadcast, checking it") + assert requested_to_addr.endswith(".onion") + peer.wait_for_disconnect() + # Now it is (C). + assert_equal(peer.message_count, { + "version": 1, + "verack": 1, + "tx": 1, + "ping": 1 + }) + dummy_address = CAddress() + dummy_address.nServices = 0 + assert_equal(peer.last_message["version"].nVersion, P2P_VERSION) + assert_equal(peer.last_message["version"].nServices, 0) + assert_equal(peer.last_message["version"].nTime, 0) + assert_equal(peer.last_message["version"].addrTo, dummy_address) + assert_equal(peer.last_message["version"].addrFrom, dummy_address) + assert_equal(peer.last_message["version"].strSubVer, "/pynode:0.0.1/") + assert_equal(peer.last_message["version"].nStartingHeight, 0) + assert_equal(peer.last_message["version"].relay, 0) + assert_equal(peer.last_message["tx"].tx.rehash(), tx["txid"]) + self.log.debug(f"Outbound connection i={i} is proper private broadcast, test ok") + num_private_broadcast_opened += 1 + if num_private_broadcast_opened == NUM_PRIVATE_BROADCAST_PER_TX: + break + assert_equal(num_private_broadcast_opened, NUM_PRIVATE_BROADCAST_PER_TX) + + wtxid_int = int(tx["wtxid"], 16) + inv = CInv(MSG_WTX, wtxid_int) + + self.log.info("Checking that the transaction is not in the originator node's mempool") + assert_equal(len(tx_originator.getrawmempool()), 0) + + self.log.info("Sending INV from an observer and waiting for GETDATA from node") + observer_inbound.tx_store[wtxid_int] = tx["tx"] + assert "getdata" not in observer_inbound.last_message + observer_inbound.send_message(msg_inv([inv])) + observer_inbound.wait_until(lambda: "getdata" in observer_inbound.last_message) + self.wait_until(lambda: len(tx_originator.getrawmempool()) > 0) + + self.log.info("Waiting for normal broadcast to another observer") + observer_outbound = self.socks5_server.conf.destinations[0]["node"] + observer_outbound.wait_for_inv([inv]) + + +if __name__ == "__main__": + P2PPrivateBroadcast().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 725b116281a468..8ecb471708089c 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -330,6 +330,7 @@ 'rpc_dumptxoutset.py', 'feature_minchainwork.py', 'rpc_estimatefee.py', + 'p2p_private_broadcast.py', 'rpc_getblockstats.py', 'feature_bind_port_externalip.py', 'wallet_create_tx.py --legacy-wallet',