Skip to content

Commit d018876

Browse files
committed
Merge bitcoin/bitcoin#34039: test: address self-announcement
1841bf9 test: address self-announcement (0xb10c) Pull request description: Test that a node sends a self-announcement with its external IP to in- and outbound peers after connection open and again sometime later. Since the code for the test is mostly the same for addr and addrv2 messages, I opted to add a new test file instead of having duplicate code in `p2p_addr_relay.py` and `p2p_addrv2_relay.py`. ACKs for top commit: Bicaru20: ACK bitcoin/bitcoin@1841bf9 achow101: ACK 1841bf9 rkrux: ACK 1841bf9 fjahr: Code review ACK 1841bf9 Tree-SHA512: 692a01e9f10eb55ee870de623e85182a10a75225766e0f0251ad5d9e369537ec27ca6e06905374190f3afe00ba6f71ae72f262228baaa535238a87160e1ce4f1
2 parents 5bbc7c8 + 1841bf9 commit d018876

File tree

2 files changed

+143
-0
lines changed

2 files changed

+143
-0
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2025-present The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""
6+
Test that a node sends a self-announcement with its external IP to
7+
in- and outbound peers after connection open and again sometime later.
8+
"""
9+
10+
import time
11+
12+
from test_framework.messages import (
13+
CAddress,
14+
from_hex,
15+
msg_headers,
16+
CBlockHeader,
17+
)
18+
from test_framework.p2p import P2PInterface
19+
from test_framework.test_framework import BitcoinTestFramework
20+
from test_framework.util import assert_equal, assert_greater_than
21+
22+
IP_TO_ANNOUNCE = "42.42.42.42"
23+
ONE_DAY = 60 * 60 * 24
24+
25+
26+
class SelfAnnouncementReceiver(P2PInterface):
27+
self_announcements_received = 0
28+
addresses_received = 0
29+
addr_messages_received = 0
30+
31+
expected = None
32+
addrv2_test = False
33+
34+
def __init__(self, *, expected, addrv2):
35+
super().__init__(support_addrv2=addrv2)
36+
self.expected = expected
37+
self.addrv2_test = addrv2
38+
39+
def handle_addr_message(self, message):
40+
self.addr_messages_received += 1
41+
for addr in message.addrs:
42+
self.addresses_received += 1
43+
if addr == self.expected:
44+
self.self_announcements_received += 1
45+
46+
def on_addrv2(self, message):
47+
assert (self.addrv2_test)
48+
self.handle_addr_message(message)
49+
50+
def on_addr(self, message):
51+
assert (not self.addrv2_test)
52+
self.handle_addr_message(message)
53+
54+
55+
class AddrSelfAnnouncementTest(BitcoinTestFramework):
56+
def set_test_params(self):
57+
self.num_nodes = 1
58+
self.extra_args = [[f"-externalip={IP_TO_ANNOUNCE}"]]
59+
60+
def run_test(self):
61+
# populate addrman to have some addresses for a GETADDR response
62+
for i in range(50):
63+
a = f"{1 + i}.{i}.1.1"
64+
self.nodes[0].addpeeraddress(a, 8333)
65+
66+
self.self_announcement_test(outbound=False, addrv2=False)
67+
self.self_announcement_test(outbound=False, addrv2=True)
68+
self.self_announcement_test(outbound=True, addrv2=False)
69+
self.self_announcement_test(outbound=True, addrv2=True)
70+
71+
def inbound_connection_open_assertions(self, addr_receiver):
72+
# We expect one self-announcement and multiple other addresses in
73+
# response to a GETADDR in a single addr / addrv2 message.
74+
assert_equal(addr_receiver.self_announcements_received, 1)
75+
assert_equal(addr_receiver.addr_messages_received, 1)
76+
assert_greater_than(addr_receiver.addresses_received, 1)
77+
78+
def outbound_connection_open_assertions(self, addr_receiver):
79+
# We expect only the self-announcement.
80+
assert_equal(addr_receiver.self_announcements_received, 1)
81+
assert_equal(addr_receiver.addr_messages_received, 1)
82+
assert_equal(addr_receiver.addresses_received, 1)
83+
84+
def self_announcement_test(self, *, outbound, addrv2):
85+
connection_type = "outbound" if outbound else "inbound"
86+
addr_version = "addrv2" if addrv2 else "addrv1"
87+
self.log.info(f"Test that the node does an address self-announcement to {connection_type} connections ({addr_version})")
88+
89+
# We only self-announce after initial block download is done
90+
assert (not self.nodes[0].getblockchaininfo()["initialblockdownload"])
91+
92+
netinfo = self.nodes[0].getnetworkinfo()
93+
port = netinfo["localaddresses"][0]["port"]
94+
self.nodes[0].setmocktime(int(time.time()))
95+
96+
expected = CAddress()
97+
expected.nServices = int(netinfo["localservices"], 16)
98+
expected.ip = IP_TO_ANNOUNCE
99+
expected.port = port
100+
expected.time = self.nodes[0].mocktime
101+
102+
with self.nodes[0].assert_debug_log([f'Advertising address {IP_TO_ANNOUNCE}:{port}']):
103+
if outbound:
104+
self.log.info(f"Check that we get an initial self-announcement on an outbound connection from the node ({connection_type}, {addr_version})")
105+
addr_receiver = self.nodes[0].add_outbound_p2p_connection(SelfAnnouncementReceiver(expected=expected, addrv2=addrv2), p2p_idx=0, connection_type="outbound-full-relay")
106+
else:
107+
self.log.info(f"Check that we get an initial self-announcement when connecting to a node and sending a GETADDR ({connection_type}, {addr_version})")
108+
addr_receiver = self.nodes[0].add_p2p_connection(SelfAnnouncementReceiver(expected=expected, addrv2=addrv2))
109+
addr_receiver.sync_with_ping()
110+
111+
if outbound:
112+
self.outbound_connection_open_assertions(addr_receiver)
113+
else:
114+
self.inbound_connection_open_assertions(addr_receiver)
115+
116+
if outbound:
117+
# to avoid the node evicting the outbound peer, protect it by announcing the most recent header to it
118+
tip_header = from_hex(CBlockHeader(), self.nodes[0].getblockheader(self.nodes[0].getbestblockhash(), False))
119+
addr_receiver.send_and_ping(msg_headers([tip_header]))
120+
121+
self.log.info(f"Check that we get more self-announcements sometime later ({connection_type}, {addr_version})")
122+
for _ in range(5):
123+
last_self_announcements_received = addr_receiver.self_announcements_received
124+
last_addr_messages_received = addr_receiver.addr_messages_received
125+
last_addresses_received = addr_receiver.addresses_received
126+
with self.nodes[0].assert_debug_log([f'Advertising address {IP_TO_ANNOUNCE}:{port}']):
127+
# m_next_local_addr_send and AVG_LOCAL_ADDRESS_BROADCAST_INTERVAL:
128+
# self-announcements are sent on an exponential distribution with mean interval of 24h.
129+
# Setting the mocktime 20d forward gives a probability of (1 - e^-(480/24)) that
130+
# the event will occur (i.e. this fails once in ~500 million repeats).
131+
self.nodes[0].bumpmocktime(20 * ONE_DAY)
132+
addr_receiver.expected.time = self.nodes[0].mocktime
133+
addr_receiver.sync_with_ping()
134+
135+
assert_equal(addr_receiver.self_announcements_received, last_self_announcements_received + 1)
136+
assert_equal(addr_receiver.addr_messages_received, last_addr_messages_received + 1)
137+
assert_equal(addr_receiver.addresses_received, last_addresses_received + 1)
138+
139+
self.nodes[0].disconnect_p2ps()
140+
141+
if __name__ == '__main__':
142+
AddrSelfAnnouncementTest(__file__).main()

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@
276276
'p2p_initial_headers_sync.py',
277277
'feature_nulldummy.py',
278278
'mempool_accept.py',
279+
'p2p_addr_selfannouncement.py',
279280
'mempool_expiry.py',
280281
'wallet_importdescriptors.py',
281282
'wallet_crosschain.py',

0 commit comments

Comments
 (0)