Skip to content
Bitcoin Roots

v31.1-roots.1 · Commit 5: [PATCH 05/17] feat(net): preserve Roots peer controls and health view on v31

test/functional/p2p_roots_controls.py

Before: /dev/null · After: test/functional/p2p_roots_controls.py

File metadata
new file mode 100644
index 0000000000000000000000000000000000000000..b6e9a325137c497589ef70aa65ae1cb899729268

Download original patch · Release notes

Diff rows 1–135 of 135. Line numbers refer to the original before/after files.

+ Added− RemovedUnmarked lines are context.

Scroll the code area horizontally to read long lines. Before is on the left; after is on the right. Unified view fits narrow screens more easily.

test/functional/p2p_roots_controls.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2026 The Bitcoin Roots developers
3
+ # Distributed under the MIT software license, see the accompanying
4
+ # file COPYING or https://opensource.org/license/mit.
5
+ """Roots permission, transport and Tor-bind resource boundaries."""
6
+ from queue import Empty
7
+
8
+ from test_framework.messages import NODE_BLOOM, NODE_COMPACT_FILTERS, msg_getcfilters
9
+ from test_framework.p2p import P2PInterface
10
+ from test_framework.socks5 import Socks5Command, Socks5Configuration, Socks5Server
11
+ from test_framework.test_framework import BitcoinTestFramework
12
+ from test_framework.util import assert_equal, assert_raises_rpc_error, p2p_port
13
+
14
+
15
+ class RootsPeerControlsTest(BitcoinTestFramework):
16
+ def set_test_params(self):
17
+ self.num_nodes = 2
18
+ self.setup_clean_chain = True
19
+
20
+ def run_test(self):
21
+ node = self.nodes[0]
22
+ self.log.info("Fee-filter quantization retains source incremental-fee profiles")
23
+ for incremental, expected in [('0.00000100', 9936506), ('0.00001000', 9170997)]:
24
+ self.restart_node(0, [f'-incrementalrelayfee={incremental}'])
25
+ peer = node.add_p2p_connection(P2PInterface())
26
+ peer.wait_until(lambda: 'feefilter' in peer.last_message)
27
+ assert_equal(peer.last_message['feefilter'].feerate, expected)
28
+ node.disconnect_p2ps()
29
+ self.restart_node(0)
30
+
31
+ self.log.info("Default bloom service is loopback-only; explicit disable removes it")
32
+ peer = node.add_p2p_connection(P2PInterface())
33
+ assert 'bloomfilter' in node.getpeerinfo()[0]['permissions']
34
+ assert int(node.getnetworkinfo()['localservices'], 16) & NODE_BLOOM == 0
35
+ assert peer.last_message['version'].nServices & NODE_BLOOM
36
+ node.disconnect_p2ps()
37
+ self.restart_node(0, ['-peerbloomfilters=0'])
38
+ peer = node.add_p2p_connection(P2PInterface())
39
+ assert_equal(node.getpeerinfo()[0]['permissions'], [])
40
+ assert peer.last_message['version'].nServices & NODE_BLOOM == 0
41
+ node.disconnect_p2ps()
42
+
43
+ self.log.info("Explicit compact filter permission serves native BIP157 without global advertisement")
44
+ self.restart_node(0, ['-blockfilterindex=1', '-whitelist=cfilters@127.0.0.1'])
45
+ peer = node.add_p2p_connection(P2PInterface())
46
+ assert peer.last_message['version'].nServices & NODE_COMPACT_FILTERS
47
+ assert int(node.getnetworkinfo()['localservices'], 16) & NODE_COMPACT_FILTERS == 0
48
+ peer.send_and_ping(msg_getcfilters(filter_type=0, start_height=0, stop_hash=int(node.getbestblockhash(), 16)))
49
+ peer.wait_until(lambda: 'cfilter' in peer.last_message)
50
+ node.disconnect_p2ps()
51
+
52
+ self.log.info("Onion bind does not inherit address whitelist or loopback bloom permission")
53
+ port = p2p_port(2)
54
+ self.restart_node(0, [f'-bind=127.0.0.1:{port}=onion', '-whitelist=all@127.0.0.1'])
55
+ node.add_p2p_connection(P2PInterface(), dstport=port)
56
+ assert_equal(node.getpeerinfo()[0]['permissions'], [])
57
+ node.disconnect_p2ps()
58
+ # First normal bind is the Tor target when no explicit onion bind exists.
59
+ self.restart_node(0, ['-listenonion=1', '-whitelist=all@127.0.0.1'])
60
+ node.add_p2p_connection(P2PInterface())
61
+ assert_equal(node.getpeerinfo()[0]['permissions'], [])
62
+ node.disconnect_p2ps()
63
+
64
+ self.log.info("Forceinbound frees a protected slot but cannot evict a noban connection")
65
+ self.restart_node(0, ['-listenonion=0', '-maxconnections=12', f'-whitebind=forceinbound@127.0.0.1:{port}'])
66
+ ordinary = node.add_p2p_connection(P2PInterface())
67
+ node.add_p2p_connection(P2PInterface(), dstport=port)
68
+ ordinary.wait_for_disconnect()
69
+ assert 'noban' in node.getpeerinfo()[0]['permissions']
70
+ assert_equal(node.getpeerinfo()[0]['forced_inbound'], True)
71
+ blocked = node.add_p2p_connection(P2PInterface(), dstport=port, wait_for_verack=False, expect_success=False)
72
+ blocked.wait_for_disconnect()
73
+ assert_equal(len(node.getpeerinfo()), 1)
74
+ node.disconnect_p2ps()
75
+
76
+ self.log.info("Duplicate CJDNS manual addresses are rejected regardless of port")
77
+ self.restart_node(0, ['-cjdnsreachable=1', '-listenonion=0'])
78
+ node.setnetworkactive(False)
79
+ node.addnode('[fc00::1]:1234', 'add')
80
+ assert_raises_rpc_error(-23, "Node already added", node.addnode, '[fc00::1]:1235', 'add')
81
+ node.addnode('[fc00::1]:1234', 'remove')
82
+
83
+ self.log.info("Clearnet v2-only suppresses explicit v1 outbound and v1 fallback")
84
+ conf = Socks5Configuration()
85
+ conf.addr = ('127.0.0.1', 0)
86
+ conf.unauth = True
87
+ conf.auth = True
88
+ conf.destinations_factory = lambda address, port: {'actual_to_addr': '127.0.0.1', 'actual_to_port': p2p_port(1)}
89
+ proxy = Socks5Server(conf)
90
+
91
+ def next_proxy_command():
92
+ # Forwarders can report an expected closed socket after a deliberate
93
+ # disconnect. It is not another SOCKS request or a transport retry.
94
+ while True:
95
+ item = proxy.queue.get(timeout=5 * self.options.timeout_factor)
96
+ if isinstance(item, (BrokenPipeError, ConnectionResetError)):
97
+ continue
98
+ assert isinstance(item, Socks5Command), item
99
+ return item
100
+
101
+ proxy.start()
102
+ try:
103
+ self.restart_node(1, ['-v2transport=1'])
104
+ self.restart_node(0, ['-v2transport=1', '-v2onlyclearnet=1', '-listenonion=0', f'-proxy=127.0.0.1:{conf.addr[1]}'])
105
+ target = '1.1.1.1:8333' # The test proxy redirects all traffic to our isolated node.
106
+ with node.assert_debug_log(['outbound v1 disabled on clearnet']):
107
+ node.addnode(target, 'onetry', False)
108
+ assert proxy.queue.empty()
109
+ assert_equal(node.getpeerinfo(), [])
110
+ with node.assert_debug_log(['SOCKS5 sending username/password authentication'], unexpected_msgs=['SOCKS5 sending proxy authentication']):
111
+ node.addnode(target, 'onetry', True)
112
+ self.wait_until(lambda: len(node.getpeerinfo()) == 1 and node.getpeerinfo()[0]['transport_protocol_type'] == 'v2')
113
+ next_proxy_command()
114
+ node.disconnectnode(nodeid=node.getpeerinfo()[0]['id'])
115
+ self.wait_until(lambda: not node.getpeerinfo())
116
+ self.restart_node(1, ['-v2transport=0'])
117
+ with node.assert_debug_log(['trying v2 connection'], unexpected_msgs=['retrying with v1']):
118
+ node.addnode(target, 'onetry', True)
119
+ next_proxy_command()
120
+ self.wait_until(lambda: not node.getpeerinfo())
121
+ # No second proxy connection is made for a v1 retry. Ignore only
122
+ # expected forwarder close errors; every extra request still fails.
123
+ while True:
124
+ try:
125
+ item = proxy.queue.get_nowait()
126
+ except Empty:
127
+ break
128
+ assert isinstance(item, (BrokenPipeError, ConnectionResetError)), item
129
+ finally:
130
+ proxy.stop()
131
+
132
+
133
+
134
+ if __name__ == '__main__':
135
+ RootsPeerControlsTest(__file__).main()