Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ libp2p-dcutr = { version = "0.14.1", path = "protocols/dcutr" }
libp2p-dns = { version = "0.44.0", path = "transports/dns" }
libp2p-floodsub = { version = "0.47.0", path = "protocols/floodsub" }
libp2p-gossipsub = { version = "0.50.0", path = "protocols/gossipsub" }
libp2p-identify = { version = "0.47.0", path = "protocols/identify" }
libp2p-identify = { version = "0.47.1", path = "protocols/identify" }
libp2p-identity = { version = "0.2.13" }
libp2p-kad = { version = "0.49.0", path = "protocols/kad" }
libp2p-mdns = { version = "0.48.0", path = "protocols/mdns" }
Expand Down
13 changes: 12 additions & 1 deletion protocols/gossipsub/src/behaviour.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ use crate::{
gossip_promises::GossipPromises,
handler::{Handler, HandlerEvent, HandlerIn},
mcache::MessageCache,
peer_score::{PeerScore, PeerScoreParams, PeerScoreState, PeerScoreThresholds, RejectReason},
peer_score::{
PeerScore, PeerScoreParameters, PeerScoreParams, PeerScoreState, PeerScoreThresholds,
RejectReason,
},
protocol::SIGNING_PREFIX,
queue::Queue,
rpc_proto::proto,
Expand Down Expand Up @@ -512,6 +515,14 @@ where
}
}

/// Returns the detailed gossipsub score parameters for a given peer, if one exists.
pub fn peer_score_params(&self, peer_id: &PeerId) -> Option<PeerScoreParameters> {
match &self.peer_score {
PeerScoreState::Active(peer_score) => Some(peer_score.score_report(peer_id).params),
PeerScoreState::Disabled => None,
}
}

/// Subscribe to a topic.
///
/// Returns [`Ok(true)`](Ok) if the subscription worked. Returns [`Ok(false)`](Ok) if we were
Expand Down
95 changes: 69 additions & 26 deletions protocols/gossipsub/src/peer_score.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,28 @@ impl PeerScoreState {
#[derive(Default)]
pub(crate) struct PeerScoreReport {
pub(crate) score: f64,
pub(crate) params: PeerScoreParameters,
#[cfg(feature = "metrics")]
pub(crate) penalties: Vec<crate::metrics::Penalty>,
}

#[derive(Copy, Clone, Default)]
pub struct PeerScoreParameters {
pub final_score: f64,
// weighted contributions per the spec.
// https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md#the-score-function
pub p1: f64,
pub p2: f64,
pub p3: f64,
pub p3b: f64,
pub p4: f64,
pub p5: f64,
pub p6: f64,
pub p7: f64,
// not part of the spec, but useful.
pub slow_peer_penalty: f64,
}

pub(crate) struct PeerScore {
/// The score parameters.
pub(crate) params: PeerScoreParams,
Expand Down Expand Up @@ -279,8 +297,7 @@ impl PeerScore {
if let Some(topic_params) = self.params.topics.get(topic) {
// we are tracking the topic

// the topic score
let mut topic_score = 0.0;
let topic_weight = topic_params.topic_weight;

// P1: time in mesh
if let MeshStatus::Active { mesh_time, .. } = topic_stats.mesh_status {
Expand All @@ -293,19 +310,22 @@ impl PeerScore {
topic_params.time_in_mesh_cap
}
};
topic_score += p1 * topic_params.time_in_mesh_weight;
let p1_contrib = p1 * topic_params.time_in_mesh_weight * topic_weight;
report.params.p1 += p1_contrib;
}

// P2: first message deliveries
let p2 = {
{
let v = topic_stats.first_message_deliveries;
if v < topic_params.first_message_deliveries_cap {
let p2 = if v < topic_params.first_message_deliveries_cap {
v
} else {
topic_params.first_message_deliveries_cap
}
};
topic_score += p2 * topic_params.first_message_deliveries_weight;
};
let p2_contrib =
p2 * topic_params.first_message_deliveries_weight * topic_weight;
report.params.p2 += p2_contrib;
}

// P3: mesh message deliveries
if topic_stats.mesh_message_deliveries_active
Expand All @@ -318,7 +338,9 @@ impl PeerScore {
let p3 = deficit * deficit;
let penalty = p3 * topic_params.mesh_message_deliveries_weight;

topic_score += penalty;
// contribution includes the topic weight.
report.params.p3 += penalty * topic_weight;

#[cfg(feature = "metrics")]
report
.penalties
Expand All @@ -335,29 +357,41 @@ impl PeerScore {
// P3b:
// NOTE: the weight of P3b is negative (validated in TopicScoreParams.validate), so
// this detracts.
let p3b = topic_stats.mesh_failure_penalty;
topic_score += p3b * topic_params.mesh_failure_penalty_weight;
{
let p3b_val = topic_stats.mesh_failure_penalty;
let p3b_contrib =
p3b_val * topic_params.mesh_failure_penalty_weight * topic_weight;
report.params.p3b += p3b_contrib;
}

// P4: invalid messages
// NOTE: the weight of P4 is negative (validated in TopicScoreParams.validate), so
// this detracts.
let p4 =
topic_stats.invalid_message_deliveries * topic_stats.invalid_message_deliveries;
topic_score += p4 * topic_params.invalid_message_deliveries_weight;

// update score, mixing with topic weight
report.score += topic_score * topic_params.topic_weight;
{
let p4_val = topic_stats.invalid_message_deliveries
* topic_stats.invalid_message_deliveries;
let p4_contrib =
p4_val * topic_params.invalid_message_deliveries_weight * topic_weight;
report.params.p4 += p4_contrib;
}
}
}

// apply the topic score cap, if any
if self.params.topic_score_cap > 0f64 && report.score > self.params.topic_score_cap {
report.score = self.params.topic_score_cap;
// aggregate topic contributions.
let mut topic_total = report.params.p1
+ report.params.p2
+ report.params.p3
+ report.params.p3b
+ report.params.p4;

// apply the topic score cap, if any.
if self.params.topic_score_cap > 0f64 && topic_total > self.params.topic_score_cap {
topic_total = self.params.topic_score_cap;
}

// P5: application-specific score
let p5 = peer_stats.application_score;
report.score += p5 * self.params.app_specific_weight;
let p5 = peer_stats.application_score * self.params.app_specific_weight;
report.params.p5 = p5;

// P6: IP collocation factor
for ip in peer_stats.known_ips.iter() {
Expand All @@ -383,24 +417,33 @@ impl PeerScore {
surplus=%surplus,
"[Penalty] The peer gets penalized because of too many peers with the same ip"
);
report.score += p6 * self.params.ip_colocation_factor_weight;
report.params.p6 += p6 * self.params.ip_colocation_factor_weight;
}
}
}

// P7: behavioural pattern penalty.
if peer_stats.behaviour_penalty > self.params.behaviour_penalty_threshold {
let excess = peer_stats.behaviour_penalty - self.params.behaviour_penalty_threshold;
let p7 = excess * excess;
report.score += p7 * self.params.behaviour_penalty_weight;
let p7 = (excess * excess) * self.params.behaviour_penalty_weight;
report.params.p7 = p7;
}

// Slow peer weighting.
if peer_stats.slow_peer_penalty > self.params.slow_peer_threshold {
let excess = peer_stats.slow_peer_penalty - self.params.slow_peer_threshold;
report.score += excess * self.params.slow_peer_weight;
report.params.slow_peer_penalty = excess * self.params.slow_peer_weight;
}

// Final total score with cap applied on topic contributions.
report.score = topic_total
+ report.params.p5
+ report.params.p6
+ report.params.p7
+ report.params.slow_peer_penalty;

report.params.final_score = report.score;

report
}

Expand Down
11 changes: 11 additions & 0 deletions protocols/identify/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## 0.47.1

- When address translation for an outbound ephemeral connection yields no
results due to a transport protocol mismatch (e.g. a QUIC listener observed
via a TCP connection), attempt cross-protocol translation using the observed
IP and the listen port, gated on the ports being equal. This correctly
handles the common deployment pattern of sharing a single port across TCP
and QUIC, and prevents AutoNAT from receiving ephemeral source ports as
external address candidates.
See [PR 6277](https://github.com/libp2p/rust-libp2p/pull/6277).

## 0.47.0

- Implement optional `signedPeerRecord` support for identify messages.
Expand Down
2 changes: 1 addition & 1 deletion protocols/identify/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "libp2p-identify"
edition.workspace = true
rust-version = { workspace = true }
description = "Nodes identification protocol for libp2p"
version = "0.47.0"
version = "0.47.1"
authors = ["Parity Technologies <admin@parity.io>"]
license = "MIT"
repository = "https://github.com/libp2p/rust-libp2p"
Expand Down
78 changes: 75 additions & 3 deletions protocols/identify/src/behaviour.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ fn is_tcp_addr(addr: &Multiaddr) -> bool {
matches!(first, Ip4(_) | Ip6(_) | Dns(_) | Dns4(_) | Dns6(_)) && matches!(second, Tcp(_))
}

/// Extract the port number from a TCP or QUIC multiaddr, if present.
///
/// Returns `None` for address types that do not carry a port in the second
/// protocol component (e.g. `/p2p-circuit`, WebSocket, etc.).
fn port_of(addr: &Multiaddr) -> Option<u16> {
addr.iter().find_map(|p| match p {
Protocol::Tcp(port) | Protocol::Udp(port) => Some(port),
_ => None,
})
}

/// Network behaviour that automatically identifies nodes periodically, returns information
/// about them, and answers identify queries from other nodes.
///
Expand Down Expand Up @@ -364,10 +375,39 @@ impl Behaviour {
addrs
};

// If address translation yielded nothing, broadcast the original candidate address.
if translated_addresses.is_empty() {
self.events
.push_back(ToSwarm::NewExternalAddrCandidate(observed.clone()));
// Translation yielded nothing. This happens when all listen addresses are of a
// different transport type than the observed address (e.g. a QUIC listener
// observed via a TCP connection). In that case the observed address carries an
// ephemeral source port that is not a stable external address.
//
// As a best-effort, attempt cross-protocol translation: use the observed IP with
// the listen port, but only when the listen port and the observed port are the
// same — which is true when TCP and QUIC share a port. Sharing a port means the
// observed IP is valid for all transports listening on that port.
//
// If no listen addresses exist at all, fall back to emitting the raw observed
// address. This preserves existing behaviour for nodes that have no listeners
// (e.g. pure dial-out nodes), where the raw observed address is the only signal
// available.
let cross_protocol_candidates: Vec<_> = self
.listen_addresses
.iter()
.filter(|listen| port_of(listen) == port_of(observed))
.filter_map(|listen| _address_translation(listen, observed))
.collect();

if !cross_protocol_candidates.is_empty() {
for addr in cross_protocol_candidates {
self.events
.push_back(ToSwarm::NewExternalAddrCandidate(addr));
}
} else {
// No listen addresses at all, or ports differ across all transports.
// Fall back to the raw observed address as the only available signal.
self.events
.push_back(ToSwarm::NewExternalAddrCandidate(observed.clone()));
}
} else {
for addr in translated_addresses {
self.events
Expand Down Expand Up @@ -733,6 +773,38 @@ impl KeyType {
mod tests {
use super::*;

#[test]
fn port_of_extracts_tcp_port() {
let addr: Multiaddr = "/ip4/1.2.3.4/tcp/42042".parse().unwrap();
assert_eq!(port_of(&addr), Some(42042));
}

#[test]
fn port_of_extracts_udp_port() {
let addr: Multiaddr = "/ip4/1.2.3.4/udp/42042/quic-v1".parse().unwrap();
assert_eq!(port_of(&addr), Some(42042));
}

#[test]
fn port_of_returns_none_for_circuit_addr() {
let addr: Multiaddr = "/p2p-circuit".parse().unwrap();
assert_eq!(port_of(&addr), None);
}

#[test]
fn port_of_shared_port_matches_across_transports() {
let tcp: Multiaddr = "/ip4/0.0.0.0/tcp/42042".parse().unwrap();
let quic: Multiaddr = "/ip4/1.2.3.4/udp/42042/quic-v1".parse().unwrap();
assert_eq!(port_of(&tcp), port_of(&quic));
}

#[test]
fn port_of_separate_ports_do_not_match() {
let tcp: Multiaddr = "/ip4/0.0.0.0/tcp/4001".parse().unwrap();
let quic: Multiaddr = "/ip4/1.2.3.4/udp/4002/quic-v1".parse().unwrap();
assert_ne!(port_of(&tcp), port_of(&quic));
}

#[test]
fn check_multiaddr_matches_peer_id() {
let peer_id = PeerId::random();
Expand Down
Loading