Skip to content

Commit

Permalink
test: add functional test for local tx relay
Browse files Browse the repository at this point in the history
  • Loading branch information
vasild committed May 31, 2023
1 parent f5dfaf1 commit 169184f
Show file tree
Hide file tree
Showing 2 changed files with 249 additions and 0 deletions.
248 changes: 248 additions & 0 deletions test/functional/p2p_local_tx_relay.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 169184f

Please sign in to comment.