Skip to content

Commit e72e7f4

Browse files
lexnvbkchrdmitry-markin
committed
litep2p/discovery: Fix memory leak in litep2p.public_addresses() (#5998)
This PR ensures that the `litep2p.public_addresses()` never grows indefinitely. - effectively fixes subtle memory leaks - fixes authority DHT records being dropped due to size limits being exceeded - provides a healthier subset of public addresses to the `/identify` protocol This PR adds a new `ExternalAddressExpired` event to the litep2p/discovery process. Substrate uses an LRU `address_confirmations` bounded to 32 address entries. The oldest entry is propagated via the `ExternalAddressExpired` event when a new address is added to the list (if capacity is exceeded). The expired address is then removed from the `litep2p.public_addresses()`, effectively limiting its size to 32 entries (the size of `address_confirmations` LRU). cc @paritytech/networking @alexggh --------- Signed-off-by: Alexandru Vasile <[email protected]> Co-authored-by: Bastian Köcher <[email protected]> Co-authored-by: Dmitry Markin <[email protected]>
1 parent 141eb46 commit e72e7f4

File tree

3 files changed

+85
-6
lines changed

3 files changed

+85
-6
lines changed

prdoc/pr_5998.prdoc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
2+
# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json
3+
4+
title: Fix memory leak in litep2p public addresses
5+
6+
doc:
7+
- audience: [ Node Dev, Node Operator ]
8+
description: |
9+
This PR bounds the number of public addresses of litep2p to 32 entries.
10+
This ensures we do not increase the number of addresses over time, and that the DHT
11+
authority records will not exceed the upper size limit.
12+
13+
crates:
14+
- name: sc-network
15+
bump: patch

substrate/client/network/src/litep2p/discovery.rs

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,16 @@ pub enum DiscoveryEvent {
125125

126126
/// New external address discovered.
127127
ExternalAddressDiscovered {
128-
/// Discovered addresses.
128+
/// Discovered address.
129+
address: Multiaddr,
130+
},
131+
132+
/// The external address has expired.
133+
///
134+
/// This happens when the internal buffers exceed the maximum number of external addresses,
135+
/// and this address is the oldest one.
136+
ExternalAddressExpired {
137+
/// Expired address.
129138
address: Multiaddr,
130139
},
131140

@@ -432,7 +441,13 @@ impl Discovery {
432441
}
433442

434443
/// Check if `address` can be considered a new external address.
435-
fn is_new_external_address(&mut self, address: &Multiaddr, peer: PeerId) -> bool {
444+
///
445+
/// If this address replaces an older address, the expired address is returned.
446+
fn is_new_external_address(
447+
&mut self,
448+
address: &Multiaddr,
449+
peer: PeerId,
450+
) -> (bool, Option<Multiaddr>) {
436451
log::trace!(target: LOG_TARGET, "verify new external address: {address}");
437452

438453
// is the address one of our known addresses
@@ -443,23 +458,39 @@ impl Discovery {
443458
.chain(self.public_addresses.iter())
444459
.any(|known_address| Discovery::is_known_address(&known_address, &address))
445460
{
446-
return true
461+
return (true, None)
447462
}
448463

449464
match self.address_confirmations.get(address) {
450465
Some(confirmations) => {
451466
confirmations.insert(peer);
452467

453468
if confirmations.len() >= MIN_ADDRESS_CONFIRMATIONS {
454-
return true
469+
return (true, None)
455470
}
456471
},
457472
None => {
473+
let oldest = (self.address_confirmations.len() >=
474+
self.address_confirmations.limiter().max_length() as usize)
475+
.then(|| {
476+
self.address_confirmations.pop_oldest().map(|(address, peers)| {
477+
if peers.len() >= MIN_ADDRESS_CONFIRMATIONS {
478+
return Some(address)
479+
} else {
480+
None
481+
}
482+
})
483+
})
484+
.flatten()
485+
.flatten();
486+
458487
self.address_confirmations.insert(address.clone(), Default::default());
488+
489+
return (false, oldest)
459490
},
460491
}
461492

462-
false
493+
(false, None)
463494
}
464495
}
465496

@@ -567,7 +598,21 @@ impl Stream for Discovery {
567598
supported_protocols,
568599
observed_address,
569600
})) => {
570-
if this.is_new_external_address(&observed_address, peer) {
601+
let (is_new, expired_address) =
602+
this.is_new_external_address(&observed_address, peer);
603+
604+
if let Some(expired_address) = expired_address {
605+
log::trace!(
606+
target: LOG_TARGET,
607+
"Removing expired external address expired={expired_address} is_new={is_new} observed={observed_address}",
608+
);
609+
610+
this.pending_events.push_back(DiscoveryEvent::ExternalAddressExpired {
611+
address: expired_address,
612+
});
613+
}
614+
615+
if is_new {
571616
this.pending_events.push_back(DiscoveryEvent::ExternalAddressDiscovered {
572617
address: observed_address.clone(),
573618
});

substrate/client/network/src/litep2p/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,25 @@ impl<B: BlockT + 'static, H: ExHashT> NetworkBackend<B, H> for Litep2pNetworkBac
935935
},
936936
}
937937
}
938+
Some(DiscoveryEvent::ExternalAddressExpired{ address }) => {
939+
let local_peer_id = self.litep2p.local_peer_id();
940+
941+
// Litep2p requires the peer ID to be present in the address.
942+
let address = if !std::matches!(address.iter().last(), Some(Protocol::P2p(_))) {
943+
address.with(Protocol::P2p(*local_peer_id.as_ref()))
944+
} else {
945+
address
946+
};
947+
948+
if self.litep2p.public_addresses().remove_address(&address) {
949+
log::info!(target: LOG_TARGET, "🔍 Expired external address for our node: {address}");
950+
} else {
951+
log::warn!(
952+
target: LOG_TARGET,
953+
"🔍 Failed to remove expired external address {address:?}"
954+
);
955+
}
956+
}
938957
Some(DiscoveryEvent::Ping { peer, rtt }) => {
939958
log::trace!(
940959
target: LOG_TARGET,

0 commit comments

Comments
 (0)