Skip to content

Commit 06788c6

Browse files
committed
Merge bitcoin/bitcoin#21528: [p2p] Reduce addr blackholes
3f7250b [test] Use the new endpoint to improve tests (Amiti Uttarwar) 3893da0 [RPC] Add field to getpeerinfo to indicate if addr relay is enabled (Amiti Uttarwar) 0980ca7 [test] Test that we intentionally select addr relay peers. (Amiti Uttarwar) c061599 [net_processing] Remove RelayAddrsWithPeer function (Amiti Uttarwar) 201e496 [net_processing] Introduce new field to indicate if addr relay is enabled (Amiti Uttarwar) 1d1ef2d [net_processing] Defer initializing m_addr_known (Amiti Uttarwar) 6653fa3 [test] Update p2p_addr_relay test to prepare (Amiti Uttarwar) 2fcaec7 [net_processing] Introduce SetupAddressRelay (Amiti Uttarwar) Pull request description: This PR builds on the test refactors extracted into #22306 (first 5 commits). This PR aims to reduce addr blackholes. When we receive an `addr` message that contains 10 or less addresses, we forward them to 1-2 peers. This is the main technique we use for self advertisements, so sending to peers that wouldn't relay would effectively "blackhole" the trickle. Although we cannot prevent this in a malicious case, we can improve it for the normal, honest cases, and reduce the overall likelihood of occurrence. Two known cases where peers would not participate in addr relay are if they have connected to you as a block-relay-only connection, or if they are a light client. This implementation defers initialization of `m_addr_known` until it is needed, then uses its presence to decide if the peer is participating in addr relay. For outbound (not block-relay-only) peers, we initialize the filter before sending the initial self announcement when processing their `version` message. For inbound peers, we initialize the filter if/when we get an addr related message (`ADDR`, `ADDRV2`, `GETADDR`). We do NOT initialize the filter based on a `SENDADDRV2` message. To communicate about these changes beyond bitcoin core & to (try to) ensure that no other software would be disrupted, I have: - Posted to the [mailing list](https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2021-April/018784.html) - Researched other open source clients to confirm compatibility, opened issues in all the projects & documented in bitcoin/bitcoin#21528 (comment). Many have confirmed that this change would not be problematic. - Raised as topic during [bitcoin-core-dev meeting](https://www.erisian.com.au/bitcoin-core-dev/log-2021-03-25.html#l-954) - Raised as topic during [bitcoin p2p meeting](https://www.erisian.com.au/bitcoin-core-dev/log-2021-04-20.html#l-439) ACKs for top commit: jnewbery: reACK 3f7250b glozow: ACK 3f7250b ajtowns: utACK 3f7250b Tree-SHA512: 29069282af684c1cd37d107c395fdd432dcccb11626f3c2dabfe92fdc4c85e74c7c4056fbdfa88017fec240506639b72ac6c311f8ce7c583112eb15f47e421af
2 parents b620b2d + 3f7250b commit 06788c6

File tree

5 files changed

+144
-24
lines changed

5 files changed

+144
-24
lines changed

src/net_processing.cpp

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,31 @@ struct Peer {
225225

226226
/** A vector of addresses to send to the peer, limited to MAX_ADDR_TO_SEND. */
227227
std::vector<CAddress> m_addrs_to_send;
228-
/** Probabilistic filter of addresses that this peer already knows.
229-
* Used to avoid relaying addresses to this peer more than once. */
230-
const std::unique_ptr<CRollingBloomFilter> m_addr_known;
228+
/** Probabilistic filter to track recent addr messages relayed with this
229+
* peer. Used to avoid relaying redundant addresses to this peer.
230+
*
231+
* We initialize this filter for outbound peers (other than
232+
* block-relay-only connections) or when an inbound peer sends us an
233+
* address related message (ADDR, ADDRV2, GETADDR).
234+
*
235+
* Presence of this filter must correlate with m_addr_relay_enabled.
236+
**/
237+
std::unique_ptr<CRollingBloomFilter> m_addr_known;
238+
/** Whether we are participating in address relay with this connection.
239+
*
240+
* We set this bool to true for outbound peers (other than
241+
* block-relay-only connections), or when an inbound peer sends us an
242+
* address related message (ADDR, ADDRV2, GETADDR).
243+
*
244+
* We use this bool to decide whether a peer is eligible for gossiping
245+
* addr messages. This avoids relaying to peers that are unlikely to
246+
* forward them, effectively blackholing self announcements. Reasons
247+
* peers might support addr relay on the link include that they connected
248+
* to us as a block-relay-only peer or they are a light client.
249+
*
250+
* This field must correlate with whether m_addr_known has been
251+
* initialized.*/
252+
std::atomic_bool m_addr_relay_enabled{false};
231253
/** Whether a getaddr request to this peer is outstanding. */
232254
bool m_getaddr_sent{false};
233255
/** Guards address sending timers. */
@@ -259,9 +281,8 @@ struct Peer {
259281
/** Work queue of items requested by this peer **/
260282
std::deque<CInv> m_getdata_requests GUARDED_BY(m_getdata_requests_mutex);
261283

262-
explicit Peer(NodeId id, bool addr_relay)
284+
explicit Peer(NodeId id)
263285
: m_id(id)
264-
, m_addr_known{addr_relay ? std::make_unique<CRollingBloomFilter>(5000, 0.001) : nullptr}
265286
{}
266287
};
267288

@@ -624,6 +645,14 @@ class PeerManagerImpl final : public PeerManager
624645
* @param[in] vRecv The raw message received
625646
*/
626647
void ProcessGetCFCheckPt(CNode& peer, CDataStream& vRecv);
648+
649+
/** Checks if address relay is permitted with peer. If needed, initializes
650+
* the m_addr_known bloom filter and sets m_addr_relay_enabled to true.
651+
*
652+
* @return True if address relay is enabled with peer
653+
* False if address relay is disallowed
654+
*/
655+
bool SetupAddressRelay(CNode& node, Peer& peer);
627656
};
628657
} // namespace
629658

@@ -744,11 +773,6 @@ static CNodeState *State(NodeId pnode) EXCLUSIVE_LOCKS_REQUIRED(cs_main) {
744773
return &it->second;
745774
}
746775

747-
static bool RelayAddrsWithPeer(const Peer& peer)
748-
{
749-
return peer.m_addr_known != nullptr;
750-
}
751-
752776
/**
753777
* Whether the peer supports the address. For example, a peer that does not
754778
* implement BIP155 cannot receive Tor v3 addresses because it requires
@@ -1129,9 +1153,7 @@ void PeerManagerImpl::InitializeNode(CNode *pnode)
11291153
assert(m_txrequest.Count(nodeid) == 0);
11301154
}
11311155
{
1132-
// Addr relay is disabled for outbound block-relay-only peers to
1133-
// prevent adversaries from inferring these links from addr traffic.
1134-
PeerRef peer = std::make_shared<Peer>(nodeid, /* addr_relay = */ !pnode->IsBlockOnlyConn());
1156+
PeerRef peer = std::make_shared<Peer>(nodeid);
11351157
LOCK(m_peer_mutex);
11361158
m_peer_map.emplace_hint(m_peer_map.end(), nodeid, std::move(peer));
11371159
}
@@ -1270,6 +1292,7 @@ bool PeerManagerImpl::GetNodeStateStats(NodeId nodeid, CNodeStateStats& stats) c
12701292
stats.m_ping_wait = ping_wait;
12711293
stats.m_addr_processed = peer->m_addr_processed.load();
12721294
stats.m_addr_rate_limited = peer->m_addr_rate_limited.load();
1295+
stats.m_addr_relay_enabled = peer->m_addr_relay_enabled.load();
12731296

12741297
return true;
12751298
}
@@ -1684,7 +1707,7 @@ void PeerManagerImpl::RelayAddress(NodeId originator,
16841707
LOCK(m_peer_mutex);
16851708

16861709
for (auto& [id, peer] : m_peer_map) {
1687-
if (RelayAddrsWithPeer(*peer) && id != originator && IsAddrCompatible(*peer, addr)) {
1710+
if (peer->m_addr_relay_enabled && id != originator && IsAddrCompatible(*peer, addr)) {
16881711
uint64_t hashKey = CSipHasher(hasher).Write(id).Finalize();
16891712
for (unsigned int i = 0; i < nRelayNodes; i++) {
16901713
if (hashKey > best[i].first) {
@@ -2574,7 +2597,8 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
25742597
UpdatePreferredDownload(pfrom, State(pfrom.GetId()));
25752598
}
25762599

2577-
if (!pfrom.IsInboundConn() && !pfrom.IsBlockOnlyConn()) {
2600+
// Self advertisement & GETADDR logic
2601+
if (!pfrom.IsInboundConn() && SetupAddressRelay(pfrom, *peer)) {
25782602
// For outbound peers, we try to relay our address (so that other
25792603
// nodes can try to find us more quickly, as we have no guarantee
25802604
// that an outbound peer is even aware of how to reach us) and do a
@@ -2583,8 +2607,9 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
25832607
// empty and no one will know who we are, so these mechanisms are
25842608
// important to help us connect to the network.
25852609
//
2586-
// We skip this for block-relay-only peers to avoid potentially leaking
2587-
// information about our block-relay-only connections via address relay.
2610+
// We skip this for block-relay-only peers. We want to avoid
2611+
// potentially leaking addr information and we do not want to
2612+
// indicate to the peer that we will participate in addr relay.
25882613
if (fListen && !m_chainman.ActiveChainstate().IsInitialBlockDownload())
25892614
{
25902615
CAddress addr = GetLocalAddress(&pfrom.addr, pfrom.GetLocalServices());
@@ -2782,10 +2807,11 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
27822807

27832808
s >> vAddr;
27842809

2785-
if (!RelayAddrsWithPeer(*peer)) {
2810+
if (!SetupAddressRelay(pfrom, *peer)) {
27862811
LogPrint(BCLog::NET, "ignoring %s message from %s peer=%d\n", msg_type, pfrom.ConnectionTypeAsString(), pfrom.GetId());
27872812
return;
27882813
}
2814+
27892815
if (vAddr.size() > MAX_ADDR_TO_SEND)
27902816
{
27912817
Misbehaving(pfrom.GetId(), 20, strprintf("%s message size = %u", msg_type, vAddr.size()));
@@ -3718,6 +3744,8 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
37183744
return;
37193745
}
37203746

3747+
SetupAddressRelay(pfrom, *peer);
3748+
37213749
// Only send one GetAddr response per connection to reduce resource waste
37223750
// and discourage addr stamping of INV announcements.
37233751
if (peer->m_getaddr_recvd) {
@@ -4305,7 +4333,7 @@ void PeerManagerImpl::MaybeSendPing(CNode& node_to, Peer& peer, std::chrono::mic
43054333
void PeerManagerImpl::MaybeSendAddr(CNode& node, Peer& peer, std::chrono::microseconds current_time)
43064334
{
43074335
// Nothing to do for non-address-relay peers
4308-
if (!RelayAddrsWithPeer(peer)) return;
4336+
if (!peer.m_addr_relay_enabled) return;
43094337

43104338
LOCK(peer.m_addr_send_times_mutex);
43114339
// Periodically advertise our local address to the peer.
@@ -4433,6 +4461,22 @@ class CompareInvMempoolOrder
44334461
};
44344462
}
44354463

4464+
bool PeerManagerImpl::SetupAddressRelay(CNode& node, Peer& peer)
4465+
{
4466+
// We don't participate in addr relay with outbound block-relay-only
4467+
// connections to prevent providing adversaries with the additional
4468+
// information of addr traffic to infer the link.
4469+
if (node.IsBlockOnlyConn()) return false;
4470+
4471+
if (!peer.m_addr_relay_enabled.exchange(true)) {
4472+
// First addr message we have received from the peer, initialize
4473+
// m_addr_known
4474+
peer.m_addr_known = std::make_unique<CRollingBloomFilter>(5000, 0.001);
4475+
}
4476+
4477+
return true;
4478+
}
4479+
44364480
bool PeerManagerImpl::SendMessages(CNode* pto)
44374481
{
44384482
PeerRef peer = GetPeerRef(pto->GetId());

src/net_processing.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ struct CNodeStateStats {
3131
std::vector<int> vHeightInFlight;
3232
uint64_t m_addr_processed = 0;
3333
uint64_t m_addr_rate_limited = 0;
34+
bool m_addr_relay_enabled{false};
3435
};
3536

3637
class PeerManager : public CValidationInterface, public NetEventsInterface

src/rpc/net.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ static RPCHelpMan getpeerinfo()
118118
{RPCResult::Type::STR, "addr", "(host:port) The IP address and port of the peer"},
119119
{RPCResult::Type::STR, "addrbind", "(ip:port) Bind address of the connection to the peer"},
120120
{RPCResult::Type::STR, "addrlocal", "(ip:port) Local address as reported by the peer"},
121+
{RPCResult::Type::BOOL, "addr_relay_enabled", "Whether we participate in address relay with this peer"},
121122
{RPCResult::Type::STR, "network", "Network (" + Join(GetNetworkNames(/* append_unroutable */ true), ", ") + ")"},
122123
{RPCResult::Type::NUM, "mapped_as", "The AS in the BGP route to the peer used for diversifying\n"
123124
"peer selection (only available if the asmap config flag is set)"},
@@ -201,6 +202,7 @@ static RPCHelpMan getpeerinfo()
201202
if (!(stats.addrLocal.empty())) {
202203
obj.pushKV("addrlocal", stats.addrLocal);
203204
}
205+
obj.pushKV("addr_relay_enabled", statestats.m_addr_relay_enabled);
204206
obj.pushKV("network", GetNetworkName(stats.m_network));
205207
if (stats.m_mapped_as != 0) {
206208
obj.pushKV("mapped_as", uint64_t(stats.m_mapped_as));

test/functional/p2p_addr_relay.py

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111
NODE_NETWORK,
1212
NODE_WITNESS,
1313
msg_addr,
14-
msg_getaddr
14+
msg_getaddr,
15+
msg_verack
1516
)
1617
from test_framework.p2p import (
1718
P2PInterface,
1819
p2p_lock,
1920
)
2021
from test_framework.test_framework import BitcoinTestFramework
21-
from test_framework.util import assert_equal
22+
from test_framework.util import assert_equal, assert_greater_than
2223
import random
2324
import time
2425

@@ -27,10 +28,12 @@ class AddrReceiver(P2PInterface):
2728
num_ipv4_received = 0
2829
test_addr_contents = False
2930
_tokens = 1
31+
send_getaddr = True
3032

31-
def __init__(self, test_addr_contents=False):
33+
def __init__(self, test_addr_contents=False, send_getaddr=True):
3234
super().__init__()
3335
self.test_addr_contents = test_addr_contents
36+
self.send_getaddr = send_getaddr
3437

3538
def on_addr(self, message):
3639
for addr in message.addrs:
@@ -60,6 +63,11 @@ def increment_tokens(self, n):
6063
def addr_received(self):
6164
return self.num_ipv4_received != 0
6265

66+
def on_version(self, message):
67+
self.send_message(msg_verack())
68+
if (self.send_getaddr):
69+
self.send_message(msg_getaddr())
70+
6371
def getaddr_received(self):
6472
return self.message_count['getaddr'] > 0
6573

@@ -75,6 +83,10 @@ def set_test_params(self):
7583
def run_test(self):
7684
self.oversized_addr_test()
7785
self.relay_tests()
86+
self.inbound_blackhole_tests()
87+
88+
# This test populates the addrman, which can impact the node's behavior
89+
# in subsequent tests
7890
self.getaddr_tests()
7991
self.blocksonly_mode_tests()
8092
self.rate_limit_tests()
@@ -156,7 +168,7 @@ def relay_tests(self):
156168
self.nodes[0].disconnect_p2ps()
157169

158170
self.log.info('Check relay of addresses received from outbound peers')
159-
inbound_peer = self.nodes[0].add_p2p_connection(AddrReceiver(test_addr_contents=True))
171+
inbound_peer = self.nodes[0].add_p2p_connection(AddrReceiver(test_addr_contents=True, send_getaddr=False))
160172
full_outbound_peer = self.nodes[0].add_outbound_p2p_connection(AddrReceiver(), p2p_idx=0, connection_type="outbound-full-relay")
161173
msg = self.setup_addr_msg(2)
162174
self.send_addr_msg(full_outbound_peer, msg, [inbound_peer])
@@ -167,6 +179,9 @@ def relay_tests(self):
167179
# of the outbound peer which is often sent before the GETADDR response.
168180
assert_equal(inbound_peer.num_ipv4_received, 0)
169181

182+
# Send an empty ADDR message to intialize address relay on this connection.
183+
inbound_peer.send_and_ping(msg_addr())
184+
170185
self.log.info('Check that subsequent addr messages sent from an outbound peer are relayed')
171186
msg2 = self.setup_addr_msg(2)
172187
self.send_addr_msg(full_outbound_peer, msg2, [inbound_peer])
@@ -184,7 +199,64 @@ def relay_tests(self):
184199

185200
self.nodes[0].disconnect_p2ps()
186201

202+
def sum_addr_messages(self, msgs_dict):
203+
return sum(bytes_received for (msg, bytes_received) in msgs_dict.items() if msg in ['addr', 'addrv2', 'getaddr'])
204+
205+
def inbound_blackhole_tests(self):
206+
self.log.info('Check that we only relay addresses to inbound peers who have previously sent us addr related messages')
207+
208+
addr_source = self.nodes[0].add_p2p_connection(P2PInterface())
209+
receiver_peer = self.nodes[0].add_p2p_connection(AddrReceiver())
210+
blackhole_peer = self.nodes[0].add_p2p_connection(AddrReceiver(send_getaddr=False))
211+
initial_addrs_received = receiver_peer.num_ipv4_received
212+
213+
peerinfo = self.nodes[0].getpeerinfo()
214+
assert_equal(peerinfo[0]['addr_relay_enabled'], True) # addr_source
215+
assert_equal(peerinfo[1]['addr_relay_enabled'], True) # receiver_peer
216+
assert_equal(peerinfo[2]['addr_relay_enabled'], False) # blackhole_peer
217+
218+
# addr_source sends 2 addresses to node0
219+
msg = self.setup_addr_msg(2)
220+
addr_source.send_and_ping(msg)
221+
self.mocktime += 30 * 60
222+
self.nodes[0].setmocktime(self.mocktime)
223+
receiver_peer.sync_with_ping()
224+
blackhole_peer.sync_with_ping()
225+
226+
peerinfo = self.nodes[0].getpeerinfo()
227+
228+
# Confirm node received addr-related messages from receiver peer
229+
assert_greater_than(self.sum_addr_messages(peerinfo[1]['bytesrecv_per_msg']), 0)
230+
# And that peer received addresses
231+
assert_equal(receiver_peer.num_ipv4_received - initial_addrs_received, 2)
232+
233+
# Confirm node has not received addr-related messages from blackhole peer
234+
assert_equal(self.sum_addr_messages(peerinfo[2]['bytesrecv_per_msg']), 0)
235+
# And that peer did not receive addresses
236+
assert_equal(blackhole_peer.num_ipv4_received, 0)
237+
238+
self.log.info("After blackhole peer sends addr message, it becomes eligible for addr gossip")
239+
blackhole_peer.send_and_ping(msg_addr())
240+
241+
# Confirm node has now received addr-related messages from blackhole peer
242+
assert_greater_than(self.sum_addr_messages(peerinfo[1]['bytesrecv_per_msg']), 0)
243+
assert_equal(self.nodes[0].getpeerinfo()[2]['addr_relay_enabled'], True)
244+
245+
msg = self.setup_addr_msg(2)
246+
self.send_addr_msg(addr_source, msg, [receiver_peer, blackhole_peer])
247+
248+
# And that peer received addresses
249+
assert_equal(blackhole_peer.num_ipv4_received, 2)
250+
251+
self.nodes[0].disconnect_p2ps()
252+
187253
def getaddr_tests(self):
254+
# In the previous tests, the node answered GETADDR requests with an
255+
# empty addrman. Due to GETADDR response caching (see
256+
# CConnman::GetAddresses), the node would continue to provide 0 addrs
257+
# in response until enough time has passed or the node is restarted.
258+
self.restart_node(0)
259+
188260
self.log.info('Test getaddr behavior')
189261
self.log.info('Check that we send a getaddr message upon connecting to an outbound-full-relay peer')
190262
full_outbound_peer = self.nodes[0].add_outbound_p2p_connection(AddrReceiver(), p2p_idx=0, connection_type="outbound-full-relay")
@@ -197,7 +269,7 @@ def getaddr_tests(self):
197269
assert_equal(block_relay_peer.getaddr_received(), False)
198270

199271
self.log.info('Check that we answer getaddr messages only from inbound peers')
200-
inbound_peer = self.nodes[0].add_p2p_connection(AddrReceiver())
272+
inbound_peer = self.nodes[0].add_p2p_connection(AddrReceiver(send_getaddr=False))
201273
inbound_peer.sync_with_ping()
202274

203275
# Add some addresses to addrman

test/functional/test_framework/p2p.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ def on_version(self, message):
438438
self.send_message(msg_sendaddrv2())
439439
self.send_message(msg_verack())
440440
self.nServices = message.nServices
441+
self.send_message(msg_getaddr())
441442

442443
# Connection helper methods
443444

0 commit comments

Comments
 (0)