From d5945c7b984e768e703ebe6d91784db10c304c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:26:45 -0300 Subject: [PATCH] fix(p2p): wait out the peers' backoff before regrafting a left topic A rust-libp2p peer that sees a mesh peer unsubscribe backs off from it for its prune backoff (60 s by default), and the leaver's PRUNE, which carries the shorter unsubscribe backoff (10 s by default), cannot shorten it. A node that leaves an aggregator subnet when its duty expires and rejoins it within that window grafts into peers that reject the graft and penalize it. On a 7-node all-ethlambda kurtosis devnet, every node penalized two peers at one epoch boundary for exactly this. Set both backoffs to the prune value, so the rejoining node waits as long as its peers do. The mesh on that subnet would stay empty for the window either way, since the early grafts are rejected; this only removes the penalty. --- crates/net/p2p/src/lib.rs | 25 +++++++++++++++++++++++++ docs/beacon_wire.md | 13 ++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index dc2f1f1f..89e3a457 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -817,6 +817,20 @@ pub enum SwarmBuildError { /// [`beacon::scoring::ScoreSettings`]. pub(crate) const MESH_N: usize = 8; +/// How long gossipsub waits before grafting a peer again after either side +/// pruned the other, and, for this node, after leaving a topic before grafting +/// into it again. +/// +/// The two are the same on purpose. A rust-libp2p peer (another ethlambda, +/// lighthouse) that sees a mesh peer unsubscribe backs off from it for its own +/// prune backoff, and the shorter one carried by the leaver's PRUNE cannot +/// shorten that. With gossipsub's shorter default for leaving, rejoining a topic +/// inside that window sends grafts every such peer rejects and penalizes, so +/// the mesh stays empty either way and the node pays a behaviour penalty for +/// it. Leaving an aggregator subnet at the end of a duty and rejoining it for +/// one two epochs later does exactly that. +const PRUNE_BACKOFF: Duration = Duration::from_secs(60); + /// The gossipsub parameters both wires share. /// /// `mesh_n` 8, low 6, high 12, the 700ms heartbeat, and the 6/3 history already @@ -846,6 +860,8 @@ pub(crate) fn gossipsub_config( .fanout_ttl(Duration::from_secs(60)) .history_length(6) .history_gossip(3) + .prune_backoff(PRUNE_BACKOFF) + .unsubscribe_backoff(PRUNE_BACKOFF) .duplicate_cache_time(seen_ttl) .validation_mode(ValidationMode::Anonymous) .message_id_fn(compute_message_id) @@ -3359,6 +3375,15 @@ mod tests { assert!(!gossipsub_config(ttl, false).validate_messages()); } + #[test] + fn rejoining_a_topic_waits_out_the_backoff_peers_apply_to_a_leaver() { + // Lighthouse leaves gossipsub's prune backoff at its default. + let peers_backoff = libp2p::gossipsub::Config::default().prune_backoff(); + let config = gossipsub_config(Duration::from_secs(1), true); + assert!(config.unsubscribe_backoff() >= peers_backoff); + assert!(config.unsubscribe_backoff() >= config.prune_backoff()); + } + /// How many times [`concurrent_beacon_requests_on_different_protocols_never_cross`] /// repeats its connect-and-fire cycle. /// diff --git a/docs/beacon_wire.md b/docs/beacon_wire.md index 6c5e9947..28b8a1f1 100644 --- a/docs/beacon_wire.md +++ b/docs/beacon_wire.md @@ -281,7 +281,7 @@ scoring P3 there would penalize every mesh peer for this node's own gap. All 64 attestation subnets get parameters, not just the backbone ones, because aggregator duties join others at runtime. -Two differences from lighthouse: +Three differences from lighthouse: - `mesh_n` is this node's `D` (8), not lighthouse's 5. It only enters the first-message-delivery cap. @@ -299,6 +299,17 @@ Two differences from lighthouse: follower it reported `synced` through a 10-minute catch-up up to 98 slots behind, since its network-stall rule reads a lagging freshest-known block as a stalled network. +- **Leaving a topic waits out the peers' backoff.** Gossipsub's default + backoff after unsubscribing (10 s) is shorter than its prune backoff + (60 s), and a rust-libp2p peer that sees a mesh peer unsubscribe backs off + from it for the longer one, which the leaver's PRUNE cannot shorten. An + aggregator subnet left when its duty expires and rejoined within that + window for the next one (a duty in the last slots of epoch N, none in N+1, + one in N+2) re-grafts into peers that reject the graft and charge a + behaviour penalty. `PRUNE_BACKOFF` sets both backoffs to the prune value, + so the node waits out the same window and is not penalized. Neither + consensus-specs nor lighthouse sets these: the spec defers gossipsub v1.1's + extra parameters, and lighthouse keeps the defaults. The block, aggregate and attestation parameters depend on the active validator count, so the p2p actor rebuilds them every slot from the head's