Skip to content

Commit 2fa60f0

Browse files
committed
Merge bitcoin/bitcoin#27452: test: cover addrv2 anchors by adding TorV3 to CAddress in messages.py
ba8ab4f test: cover addrv2 support in anchors.dat with a TorV3 address (Matthew Zipkin) b4bee4b test: add keep_alive option to socks5 proxy in test_framework (Matthew Zipkin) 5aaf988 test: cover TorV3 address in p2p_addrv2_relay (Matthew Zipkin) 80f64a3 test: add support for all networks in CAddress in messages.py (brunoerg) Pull request description: Closes bitcoin/bitcoin#27140 Adds test coverage for bitcoin/bitcoin#20516 to ensure that bitcoin/bitcoin#20511 is completed and may be closed. This PR adds a test case to `feature_anchors.py` where an onion v3 address is set as a blocks-only relay peer and then shutdown, ensuring that the address is saved to anchors.dat in addrv2 format. We then ensure that bitcoin attempts to reconnect to that anchor address on restart. To compute the addrv2 serialization of the onion v3 address, I added logic to `CAddress` in `messages.py`. This new logic is covered by extending `p2p_addrv2_relay.py` to include an onion v3 address. Future work will be adding coverage for ipv6, torv2 and cjdns in these modules and also `feature_proxy.py` Also includes de/serialization unit test for `CAddress` in test framework. ACKs for top commit: jonatack: ACK ba8ab4f brunoerg: crACK ba8ab4f willcl-ark: ACK ba8ab4f Tree-SHA512: 7220e30d7cb975903d9ac575a7215a08e8f784c24c5741561affcbde12fb92cbf8704cb42e66494b788ba6ed4bb255fb0cc327e4f2190fae50c0ed9f336c0ff0
2 parents 2dea6c5 + ba8ab4f commit 2fa60f0

File tree

5 files changed

+143
-17
lines changed

5 files changed

+143
-17
lines changed

test/functional/feature_anchors.py

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66

77
import os
88

9-
from test_framework.p2p import P2PInterface
9+
from test_framework.p2p import P2PInterface, P2P_SERVICES
10+
from test_framework.socks5 import Socks5Configuration, Socks5Server
11+
from test_framework.messages import CAddress, hash256
1012
from test_framework.test_framework import BitcoinTestFramework
11-
from test_framework.util import check_node_connections
13+
from test_framework.util import check_node_connections, assert_equal, p2p_port
1214

1315
INBOUND_CONNECTIONS = 5
1416
BLOCK_RELAY_CONNECTIONS = 2
17+
ONION_ADDR = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:8333"
1518

1619

1720
class AnchorsTest(BitcoinTestFramework):
@@ -54,7 +57,7 @@ def run_test(self):
5457
else:
5558
inbound_nodes_port.append(hex(int(addr_split[1]))[2:])
5659

57-
self.log.info("Stop node 0")
60+
self.log.debug("Stop node")
5861
self.stop_node(0)
5962

6063
# It should contain only the block-relay-only addresses
@@ -78,12 +81,64 @@ def run_test(self):
7881
tweaked_contents[20:20] = b'1'
7982
out_file_handler.write(bytes(tweaked_contents))
8083

81-
self.log.info("Start node")
84+
self.log.debug("Start node")
8285
self.start_node(0)
8386

8487
self.log.info("When node starts, check if anchors.dat doesn't exist anymore")
8588
assert not os.path.exists(node_anchors_path)
8689

90+
self.log.info("Ensure addrv2 support")
91+
# Use proxies to catch outbound connections to networks with 256-bit addresses
92+
onion_conf = Socks5Configuration()
93+
onion_conf.auth = True
94+
onion_conf.unauth = True
95+
onion_conf.addr = ('127.0.0.1', p2p_port(self.num_nodes))
96+
onion_conf.keep_alive = True
97+
onion_proxy = Socks5Server(onion_conf)
98+
onion_proxy.start()
99+
self.restart_node(0, extra_args=[f"-onion={onion_conf.addr[0]}:{onion_conf.addr[1]}"])
100+
101+
self.log.info("Add 256-bit-address block-relay-only connections to node")
102+
self.nodes[0].addconnection(ONION_ADDR, 'block-relay-only')
103+
104+
self.log.debug("Stop node")
105+
with self.nodes[0].assert_debug_log([f"DumpAnchors: Flush 1 outbound block-relay-only peer addresses to anchors.dat"]):
106+
self.stop_node(0)
107+
# Manually close keep_alive proxy connection
108+
onion_proxy.stop()
109+
110+
self.log.info("Check for addrv2 addresses in anchors.dat")
111+
caddr = CAddress()
112+
caddr.net = CAddress.NET_TORV3
113+
caddr.ip, port_str = ONION_ADDR.split(":")
114+
caddr.port = int(port_str)
115+
# TorV3 addrv2 serialization:
116+
# time(4) | services(1) | networkID(1) | address length(1) | address(32)
117+
expected_pubkey = caddr.serialize_v2()[7:39].hex()
118+
119+
# position of services byte of first addr in anchors.dat
120+
# network magic, vector length, version, nTime
121+
services_index = 4 + 1 + 4 + 4
122+
data = bytes()
123+
with open(node_anchors_path, "rb") as file_handler:
124+
data = file_handler.read()
125+
assert_equal(data[services_index], 0x00) # services == NONE
126+
anchors2 = data.hex()
127+
assert expected_pubkey in anchors2
128+
129+
with open(node_anchors_path, "wb") as file_handler:
130+
# Modify service flags for this address even though we never connected to it.
131+
# This is necessary because on restart we will not attempt an anchor connection
132+
# to a host without our required services, even if its address is in the anchors.dat file
133+
new_data = bytearray(data)[:-32]
134+
new_data[services_index] = P2P_SERVICES
135+
new_data_hash = hash256(new_data)
136+
file_handler.write(new_data + new_data_hash)
137+
138+
self.log.info("Restarting node attempts to reconnect to anchors")
139+
with self.nodes[0].assert_debug_log([f"Trying to make an anchor connection to {ONION_ADDR}"]):
140+
self.start_node(0, extra_args=[f"-onion={onion_conf.addr[0]}:{onion_conf.addr[1]}"])
141+
87142

88143
if __name__ == "__main__":
89144
AnchorsTest().main()

test/functional/p2p_addrv2_relay.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,24 @@
2020
from test_framework.util import assert_equal
2121

2222
I2P_ADDR = "c4gfnttsuwqomiygupdqqqyy5y5emnk5c73hrfvatri67prd7vyq.b32.i2p"
23+
ONION_ADDR = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion"
2324

2425
ADDRS = []
2526
for i in range(10):
2627
addr = CAddress()
2728
addr.time = int(time.time()) + i
29+
addr.port = 8333 + i
2830
addr.nServices = P2P_SERVICES
29-
# Add one I2P address at an arbitrary position.
31+
# Add one I2P and one onion V3 address at an arbitrary position.
3032
if i == 5:
3133
addr.net = addr.NET_I2P
3234
addr.ip = I2P_ADDR
35+
addr.port = 0
36+
elif i == 8:
37+
addr.net = addr.NET_TORV3
38+
addr.ip = ONION_ADDR
3339
else:
3440
addr.ip = f"123.123.123.{i % 256}"
35-
addr.port = 8333 + i
3641
ADDRS.append(addr)
3742

3843

@@ -52,6 +57,17 @@ def wait_for_addrv2(self):
5257
self.wait_until(lambda: "addrv2" in self.last_message)
5358

5459

60+
def calc_addrv2_msg_size(addrs):
61+
size = 1 # vector length byte
62+
for addr in addrs:
63+
size += 4 # time
64+
size += 1 # services, COMPACTSIZE(P2P_SERVICES)
65+
size += 1 # network id
66+
size += 1 # address length byte
67+
size += addr.ADDRV2_ADDRESS_LENGTH[addr.net] # address
68+
size += 2 # port
69+
return size
70+
5571
class AddrTest(BitcoinTestFramework):
5672
def set_test_params(self):
5773
self.setup_clean_chain = True
@@ -71,9 +87,10 @@ def run_test(self):
7187
self.log.info('Check that addrv2 message content is relayed and added to addrman')
7288
addr_receiver = self.nodes[0].add_p2p_connection(AddrReceiver())
7389
msg.addrs = ADDRS
90+
msg_size = calc_addrv2_msg_size(ADDRS)
7491
with self.nodes[0].assert_debug_log([
75-
'received: addrv2 (159 bytes) peer=0',
76-
'sending addrv2 (159 bytes) peer=1',
92+
f'received: addrv2 ({msg_size} bytes) peer=0',
93+
f'sending addrv2 ({msg_size} bytes) peer=1',
7794
]):
7895
addr_source.send_and_ping(msg)
7996
self.nodes[0].setmocktime(int(time.time()) + 30 * 60)

test/functional/test_framework/messages.py

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import socket
2828
import struct
2929
import time
30+
import unittest
3031

3132
from test_framework.siphash import siphash256
3233
from test_framework.util import assert_equal
@@ -77,6 +78,10 @@ def sha256(s):
7778
return hashlib.sha256(s).digest()
7879

7980

81+
def sha3(s):
82+
return hashlib.sha3_256(s).digest()
83+
84+
8085
def hash256(s):
8186
return sha256(sha256(s))
8287

@@ -229,16 +234,25 @@ class CAddress:
229234

230235
# see https://github.com/bitcoin/bips/blob/master/bip-0155.mediawiki
231236
NET_IPV4 = 1
237+
NET_IPV6 = 2
238+
NET_TORV3 = 4
232239
NET_I2P = 5
240+
NET_CJDNS = 6
233241

234242
ADDRV2_NET_NAME = {
235243
NET_IPV4: "IPv4",
236-
NET_I2P: "I2P"
244+
NET_IPV6: "IPv6",
245+
NET_TORV3: "TorV3",
246+
NET_I2P: "I2P",
247+
NET_CJDNS: "CJDNS"
237248
}
238249

239250
ADDRV2_ADDRESS_LENGTH = {
240251
NET_IPV4: 4,
241-
NET_I2P: 32
252+
NET_IPV6: 16,
253+
NET_TORV3: 32,
254+
NET_I2P: 32,
255+
NET_CJDNS: 16
242256
}
243257

244258
I2P_PAD = "===="
@@ -285,33 +299,54 @@ def deserialize_v2(self, f):
285299
self.nServices = deser_compact_size(f)
286300

287301
self.net = struct.unpack("B", f.read(1))[0]
288-
assert self.net in (self.NET_IPV4, self.NET_I2P)
302+
assert self.net in self.ADDRV2_NET_NAME
289303

290304
address_length = deser_compact_size(f)
291305
assert address_length == self.ADDRV2_ADDRESS_LENGTH[self.net]
292306

293307
addr_bytes = f.read(address_length)
294308
if self.net == self.NET_IPV4:
295309
self.ip = socket.inet_ntoa(addr_bytes)
296-
else:
310+
elif self.net == self.NET_IPV6:
311+
self.ip = socket.inet_ntop(socket.AF_INET6, addr_bytes)
312+
elif self.net == self.NET_TORV3:
313+
prefix = b".onion checksum"
314+
version = bytes([3])
315+
checksum = sha3(prefix + addr_bytes + version)[:2]
316+
self.ip = b32encode(addr_bytes + checksum + version).decode("ascii").lower() + ".onion"
317+
elif self.net == self.NET_I2P:
297318
self.ip = b32encode(addr_bytes)[0:-len(self.I2P_PAD)].decode("ascii").lower() + ".b32.i2p"
319+
elif self.net == self.NET_CJDNS:
320+
self.ip = socket.inet_ntop(socket.AF_INET6, addr_bytes)
321+
else:
322+
raise Exception(f"Address type not supported")
298323

299324
self.port = struct.unpack(">H", f.read(2))[0]
300325

301326
def serialize_v2(self):
302327
"""Serialize in addrv2 format (BIP155)"""
303-
assert self.net in (self.NET_IPV4, self.NET_I2P)
328+
assert self.net in self.ADDRV2_NET_NAME
304329
r = b""
305330
r += struct.pack("<I", self.time)
306331
r += ser_compact_size(self.nServices)
307332
r += struct.pack("B", self.net)
308333
r += ser_compact_size(self.ADDRV2_ADDRESS_LENGTH[self.net])
309334
if self.net == self.NET_IPV4:
310335
r += socket.inet_aton(self.ip)
311-
else:
336+
elif self.net == self.NET_IPV6:
337+
r += socket.inet_pton(socket.AF_INET6, self.ip)
338+
elif self.net == self.NET_TORV3:
339+
sfx = ".onion"
340+
assert self.ip.endswith(sfx)
341+
r += b32decode(self.ip[0:-len(sfx)], True)[0:32]
342+
elif self.net == self.NET_I2P:
312343
sfx = ".b32.i2p"
313344
assert self.ip.endswith(sfx)
314345
r += b32decode(self.ip[0:-len(sfx)] + self.I2P_PAD, True)
346+
elif self.net == self.NET_CJDNS:
347+
r += socket.inet_pton(socket.AF_INET6, self.ip)
348+
else:
349+
raise Exception(f"Address type not supported")
315350
r += struct.pack(">H", self.port)
316351
return r
317352

@@ -1852,3 +1887,19 @@ def serialize(self):
18521887
def __repr__(self):
18531888
return "msg_sendtxrcncl(version=%lu, salt=%lu)" %\
18541889
(self.version, self.salt)
1890+
1891+
class TestFrameworkScript(unittest.TestCase):
1892+
def test_addrv2_encode_decode(self):
1893+
def check_addrv2(ip, net):
1894+
addr = CAddress()
1895+
addr.net, addr.ip = net, ip
1896+
ser = addr.serialize_v2()
1897+
actual = CAddress()
1898+
actual.deserialize_v2(BytesIO(ser))
1899+
self.assertEqual(actual, addr)
1900+
1901+
check_addrv2("1.65.195.98", CAddress.NET_IPV4)
1902+
check_addrv2("2001:41f0::62:6974:636f:696e", CAddress.NET_IPV6)
1903+
check_addrv2("2bqghnldu6mcug4pikzprwhtjjnsyederctvci6klcwzepnjd46ikjyd.onion", CAddress.NET_TORV3)
1904+
check_addrv2("255fhcp6ajvftnyo7bwz3an3t4a4brhopm3bamyh2iu5r3gnr2rq.b32.i2p", CAddress.NET_I2P)
1905+
check_addrv2("fc32:17ea:e415:c3bf:9808:149d:b5a2:c9aa", CAddress.NET_CJDNS)

test/functional/test_framework/socks5.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def __init__(self):
4040
self.af = socket.AF_INET # Bind address family
4141
self.unauth = False # Support unauthenticated
4242
self.auth = False # Support authentication
43+
self.keep_alive = False # Do not automatically close connections
4344

4445
class Socks5Command():
4546
"""Information about an incoming socks5 command."""
@@ -115,13 +116,14 @@ def handle(self):
115116

116117
cmdin = Socks5Command(cmd, atyp, addr, port, username, password)
117118
self.serv.queue.put(cmdin)
118-
logger.info('Proxy: %s', cmdin)
119+
logger.debug('Proxy: %s', cmdin)
119120
# Fall through to disconnect
120121
except Exception as e:
121122
logger.exception("socks5 request handling failed.")
122123
self.serv.queue.put(e)
123124
finally:
124-
self.conn.close()
125+
if not self.serv.keep_alive:
126+
self.conn.close()
125127

126128
class Socks5Server():
127129
def __init__(self, conf):
@@ -133,6 +135,7 @@ def __init__(self, conf):
133135
self.running = False
134136
self.thread = None
135137
self.queue = queue.Queue() # report connections and exceptions to client
138+
self.keep_alive = conf.keep_alive
136139

137140
def run(self):
138141
while self.running:
@@ -157,4 +160,3 @@ def stop(self):
157160
s.connect(self.conf.addr)
158161
s.close()
159162
self.thread.join()
160-

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"blocktools",
7777
"ellswift",
7878
"key",
79+
"messages",
7980
"muhash",
8081
"ripemd160",
8182
"script",

0 commit comments

Comments
 (0)