Skip to content

Commit 7dcf561

Browse files
committed
merge bitcoin#27452: cover addrv2 anchors by adding TorV3 to CAddress in messages.py
1 parent 779e429 commit 7dcf561

File tree

5 files changed

+140
-16
lines changed

5 files changed

+140
-16
lines changed

test/functional/feature_anchors.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
import os
88

99
from test_framework.p2p import P2PInterface
10+
from test_framework.socks5 import Socks5Configuration, Socks5Server
11+
from test_framework.messages import CAddress, hash256, NODE_NETWORK
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):
@@ -55,7 +58,7 @@ def run_test(self):
5558
else:
5659
inbound_nodes_port.append(hex(int(addr_split[1]))[2:])
5760

58-
self.log.info("Stop node 0")
61+
self.log.debug("Stop node")
5962
self.stop_node(0)
6063

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

82-
self.log.info("Start node")
85+
self.log.debug("Start node")
8386
self.start_node(0)
8487

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

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

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

test/functional/p2p_addrv2_relay.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from test_framework.util import assert_equal
1919

2020
I2P_ADDR = "c4gfnttsuwqomiygupdqqqyy5y5emnk5c73hrfvatri67prd7vyq.b32.i2p"
21+
ONION_ADDR = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion"
2122

2223
ADDRS: List[CAddress] = []
2324

@@ -37,6 +38,16 @@ def on_addrv2(self, message):
3738
def wait_for_addrv2(self):
3839
self.wait_until(lambda: "addrv2" in self.last_message)
3940

41+
def calc_addrv2_msg_size(addrs):
42+
size = 1 # vector length byte
43+
for addr in addrs:
44+
size += 4 # time
45+
size += 1 # services, COMPACTSIZE(P2P_SERVICES)
46+
size += 1 # network id
47+
size += 1 # address length byte
48+
size += addr.ADDRV2_ADDRESS_LENGTH[addr.net] # address
49+
size += 2 # port
50+
return size
4051

4152
class AddrTest(BitcoinTestFramework):
4253
def set_test_params(self):
@@ -48,14 +59,18 @@ def run_test(self):
4859
for i in range(10):
4960
addr = CAddress()
5061
addr.time = int(self.mocktime) + i
62+
addr.port = 8333 + i
5163
addr.nServices = NODE_NETWORK
52-
# Add one I2P address at an arbitrary position.
64+
# Add one I2P and one onion V3 address at an arbitrary position.
5365
if i == 5:
5466
addr.net = addr.NET_I2P
5567
addr.ip = I2P_ADDR
68+
addr.port = 0
69+
elif i == 8:
70+
addr.net = addr.NET_TORV3
71+
addr.ip = ONION_ADDR
5672
else:
5773
addr.ip = f"123.123.123.{i % 256}"
58-
addr.port = 8333 + i
5974
ADDRS.append(addr)
6075

6176
self.log.info('Create connection that sends addrv2 messages')
@@ -73,14 +88,15 @@ def run_test(self):
7388
addr_source = self.nodes[0].add_p2p_connection(P2PInterface())
7489
addr_receiver = self.nodes[0].add_p2p_connection(AddrReceiver())
7590
msg.addrs = ADDRS
91+
msg_size = calc_addrv2_msg_size(ADDRS)
7692
with self.nodes[0].assert_debug_log([
77-
'received: addrv2 (159 bytes) peer=1',
93+
f'received: addrv2 ({msg_size} bytes) peer=1',
7894
]):
7995
addr_source.send_and_ping(msg)
8096

8197
# Wait until "Added ..." before bumping mocktime to make sure addv2 is (almost) fully processed
8298
with self.nodes[0].assert_debug_log([
83-
'sending addrv2 (159 bytes) peer=2',
99+
f'sending addrv2 ({msg_size} bytes) peer=2',
84100
]):
85101
self.bump_mocktime(30 * 60)
86102
addr_receiver.wait_for_addrv2()

test/functional/test_framework/messages.py

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import socket
2626
import struct
2727
import time
28+
import unittest
2829

2930
from test_framework.crypto.siphash import siphash256
3031
from test_framework.util import assert_equal
@@ -74,6 +75,9 @@ def sha256(s):
7475
return hashlib.sha256(s).digest()
7576

7677

78+
def sha3(s):
79+
return hashlib.sha3_256(s).digest()
80+
7781

7882
def hash256(s):
7983
return sha256(sha256(s))
@@ -249,16 +253,25 @@ class CAddress:
249253

250254
# see https://github.com/bitcoin/bips/blob/master/bip-0155.mediawiki
251255
NET_IPV4 = 1
256+
NET_IPV6 = 2
257+
NET_TORV3 = 4
252258
NET_I2P = 5
259+
NET_CJDNS = 6
253260

254261
ADDRV2_NET_NAME = {
255262
NET_IPV4: "IPv4",
256-
NET_I2P: "I2P"
263+
NET_IPV6: "IPv6",
264+
NET_TORV3: "TorV3",
265+
NET_I2P: "I2P",
266+
NET_CJDNS: "CJDNS"
257267
}
258268

259269
ADDRV2_ADDRESS_LENGTH = {
260270
NET_IPV4: 4,
261-
NET_I2P: 32
271+
NET_IPV6: 16,
272+
NET_TORV3: 32,
273+
NET_I2P: 32,
274+
NET_CJDNS: 16
262275
}
263276

264277
I2P_PAD = "===="
@@ -305,33 +318,54 @@ def deserialize_v2(self, f):
305318
self.nServices = deser_compact_size(f)
306319

307320
self.net = struct.unpack("B", f.read(1))[0]
308-
assert self.net in (self.NET_IPV4, self.NET_I2P)
321+
assert self.net in self.ADDRV2_NET_NAME
309322

310323
address_length = deser_compact_size(f)
311324
assert address_length == self.ADDRV2_ADDRESS_LENGTH[self.net]
312325

313326
addr_bytes = f.read(address_length)
314327
if self.net == self.NET_IPV4:
315328
self.ip = socket.inet_ntoa(addr_bytes)
316-
else:
329+
elif self.net == self.NET_IPV6:
330+
self.ip = socket.inet_ntop(socket.AF_INET6, addr_bytes)
331+
elif self.net == self.NET_TORV3:
332+
prefix = b".onion checksum"
333+
version = bytes([3])
334+
checksum = sha3(prefix + addr_bytes + version)[:2]
335+
self.ip = b32encode(addr_bytes + checksum + version).decode("ascii").lower() + ".onion"
336+
elif self.net == self.NET_I2P:
317337
self.ip = b32encode(addr_bytes)[0:-len(self.I2P_PAD)].decode("ascii").lower() + ".b32.i2p"
338+
elif self.net == self.NET_CJDNS:
339+
self.ip = socket.inet_ntop(socket.AF_INET6, addr_bytes)
340+
else:
341+
raise Exception(f"Address type not supported")
318342

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

321345
def serialize_v2(self):
322346
"""Serialize in addrv2 format (BIP155)"""
323-
assert self.net in (self.NET_IPV4, self.NET_I2P)
347+
assert self.net in self.ADDRV2_NET_NAME
324348
r = b""
325349
r += struct.pack("<I", self.time)
326350
r += ser_compact_size(self.nServices)
327351
r += struct.pack("B", self.net)
328352
r += ser_compact_size(self.ADDRV2_ADDRESS_LENGTH[self.net])
329353
if self.net == self.NET_IPV4:
330354
r += socket.inet_aton(self.ip)
331-
else:
355+
elif self.net == self.NET_IPV6:
356+
r += socket.inet_pton(socket.AF_INET6, self.ip)
357+
elif self.net == self.NET_TORV3:
358+
sfx = ".onion"
359+
assert self.ip.endswith(sfx)
360+
r += b32decode(self.ip[0:-len(sfx)], True)[0:32]
361+
elif self.net == self.NET_I2P:
332362
sfx = ".b32.i2p"
333363
assert self.ip.endswith(sfx)
334364
r += b32decode(self.ip[0:-len(sfx)] + self.I2P_PAD, True)
365+
elif self.net == self.NET_CJDNS:
366+
r += socket.inet_pton(socket.AF_INET6, self.ip)
367+
else:
368+
raise Exception(f"Address type not supported")
335369
r += struct.pack(">H", self.port)
336370
return r
337371

@@ -2592,3 +2626,19 @@ def serialize(self):
25922626
def __repr__(self):
25932627
return "msg_sendtxrcncl(version=%lu, salt=%lu)" %\
25942628
(self.version, self.salt)
2629+
2630+
class TestFrameworkScript(unittest.TestCase):
2631+
def test_addrv2_encode_decode(self):
2632+
def check_addrv2(ip, net):
2633+
addr = CAddress()
2634+
addr.net, addr.ip = net, ip
2635+
ser = addr.serialize_v2()
2636+
actual = CAddress()
2637+
actual.deserialize_v2(BytesIO(ser))
2638+
self.assertEqual(actual, addr)
2639+
2640+
check_addrv2("1.65.195.98", CAddress.NET_IPV4)
2641+
check_addrv2("2001:41f0::62:6974:636f:696e", CAddress.NET_IPV6)
2642+
check_addrv2("2bqghnldu6mcug4pikzprwhtjjnsyederctvci6klcwzepnjd46ikjyd.onion", CAddress.NET_TORV3)
2643+
check_addrv2("255fhcp6ajvftnyo7bwz3an3t4a4brhopm3bamyh2iu5r3gnr2rq.b32.i2p", CAddress.NET_I2P)
2644+
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
"crypto.chacha20",
7777
"crypto.ellswift",
7878
"key",
79+
"messages",
7980
"crypto.muhash",
8081
"crypto.poly1305",
8182
"crypto.ripemd160",

0 commit comments

Comments
 (0)