Skip to content

fix(network): reduce Unknown peers via identify retries + peer:identify event - #8954

Closed
lodekeeper wants to merge 3 commits into
ChainSafe:cayman/libp2p3-againfrom
lodekeeper:fix/libp2p3-identify-retries
Closed

fix(network): reduce Unknown peers via identify retries + peer:identify event#8954
lodekeeper wants to merge 3 commits into
ChainSafe:cayman/libp2p3-againfrom
lodekeeper:fix/libp2p3-identify-retries

Conversation

@lodekeeper

@lodekeeper lodekeeper commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

Motivation

PR #8890 disabled auto-identify on connection (runOnConnectionOpen: false) to avoid wasted identify streams on irrelevant peers. Instead, identify is triggered after a successful STATUS handshake.

However, this single-attempt fire-and-forget approach causes ~24% of connected peers to remain "Unknown" on mainnet (observed on feat1-mainnet-super). Root causes:

  1. No retry on transient errors: Identify often fails with EOF or "stream closing" errors, especially under load. A single attempt means these peers stay Unknown forever.
  2. No identify-push listener: libp2p's peer:identify event (fired on successful identify or remote identify-push) was not being consumed, missing a free source of agentVersion data.
  3. Deduplication race: The Set-based identifyInProgress tracking could be broken by disconnect/reconnect sequences — a stale .finally() from an old loop could clear the flag for a new loop.

Description

Retry logic

  • 3 attempts with [0s, 5s, 15s] backoff delays
  • Skips retry on non-retryable errors (Public key missing, peer mismatch)
  • Re-checks peer state (connected? already identified?) before each retry
  • Re-fetches connection reference after delays (old connection may have closed)

peer:identify event listener

  • Listens for libp2p's peer:identify event as a safety net
  • Captures agentVersion from successful identify calls or remote identify-push
  • Only updates peers that are still connected and have agentVersion === null

Generation-based deduplication

  • Replaced Set<PeerIdStr> with Map<PeerIdStr, number> using a monotonic generation counter
  • .finally() only clears the entry if the generation matches (prevents stale loops from clearing newer ones)
  • Disconnect clears the entry immediately; reconnect assigns a new generation

Shutdown safety

  • Added AbortController checked at each retry iteration
  • close() aborts the controller, causing in-flight retry loops to bail out

Tests

Added 7 new test cases (11 total, all passing):

  • Identify triggered only after STATUS (not on connection open)
  • No re-identify after second STATUS if agentVersion already known
  • Retry on transient errors with backoff
  • Deduplication of concurrent identify calls from rapid STATUS events
  • Non-retryable errors bail out immediately (no retry)
  • Disconnect/reconnect race: stale loop's .finally() does not clear newer generation
  • peer:identify event updates agentVersion

Expected Impact

Target: reduce Unknown peer rate from ~24% to <5% on mainnet, improving peer scoring, client diversity tracking, and gossipsub optimization.

Note

This PR contains an AI-generated contribution (implementation, tests, and PR description).

@lodekeeper
lodekeeper requested a review from a team as a code owner February 24, 2026 14:43
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @lodekeeper, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the robustness of peer identification within the network layer. It addresses issues of elevated 'Unknown' peer client rates and flaky identification failures that arose after the libp2p v3 migration. By introducing retry logic for transient identify errors, preventing duplicate identification attempts, and leveraging libp2p's peer:identify events, the changes aim to improve the accuracy and reliability of peer classification, ultimately leading to a more stable and informative peer network.

Highlights

  • Identify Retry Logic: Implemented retry mechanism for peer identification on transient failures, with up to 3 attempts (0s, 5s, 15s delays). Retries stop if the peer disconnects or is already identified, and non-retryable errors (e.g., missing public key) are skipped.
  • Deduplication of Identify Loops: Added an identifyInProgress set to prevent multiple concurrent identify attempts for the same peer, ensuring that repeated status messages do not spawn overlapping retries.
  • Libp2p peer:identify Event Consumption: Integrated a listener for the libp2p peer:identify event to update agentVersion and agentClient when identification succeeds through this event path, including identify-push.
  • Cleanup on Disconnect: Ensured that any in-flight identify markers are cleared from the identifyInProgress set when a peer disconnects, preventing stale states.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • packages/beacon-node/src/network/peers/peerManager.ts
    • Imported IdentifyResult type from @libp2p/interface.
    • Defined constants IDENTIFY_MAX_ATTEMPTS and IDENTIFY_RETRY_DELAYS_MS for identify retry configuration.
    • Added a private readonly identifyInProgress set to track peers currently undergoing identification.
    • Registered an event listener for peer:identify events during PeerManager initialization.
    • Unregistered the peer:identify event listener during PeerManager shutdown.
    • Modified onLibp2pPeerConnect to check identifyInProgress before initiating peer identification and to manage the set during the identify process.
    • Added logic to delete the peer from identifyInProgress when a peer disconnects.
    • Refactored identifyPeer to include a retry loop with delays, checks for peer status, and specific handling for transient vs. non-retryable errors.
    • Introduced onPeerIdentify method to process peer:identify events and update agentVersion.
  • packages/beacon-node/test/e2e/network/peers/peerManager.test.ts
    • Imported IdentifyResult type for test mocks.
    • Added a new e2e test case Should retry identify on transient errors to verify the retry mechanism for peer identification.
    • Added a new e2e test case Should update agentVersion via peer:identify event to confirm that peer:identify events correctly update peer agent versions.
Activity
  • Extended peerManager e2e tests were added, specifically for identify retry logic and peer:identify event handling.
  • Local verification included running pnpm vitest run test/e2e/network/peers/peerManager.test.ts, npx tsc --noEmit -p packages/beacon-node/tsconfig.json, and pnpm lint.
  • Noted that two upstream uncaught exceptions (StreamStateError from @chainsafe/libp2p-noise and RangeError from @chainsafe/discv5) are mitigated by this PR's resilience improvements, rather than being directly patched.
  • The patch was developed with AI assistance and manually reviewed/validated.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a robust retry mechanism for the peer identify process to mitigate transient failures and reduce the number of 'Unknown' peers. It adds retry logic with backoff, deduplicates concurrent identify calls for the same peer using a lock, and leverages the peer:identify libp2p event as a fallback. The changes are well-implemented and include corresponding unit tests. I have one suggestion to improve the maintainability of the retry configuration constants.

Comment on lines +50 to +53
/** Maximum number of identify attempts per peer */
const IDENTIFY_MAX_ATTEMPTS = 3;
/** Retry delays for identify attempts (ms). First attempt is immediate. */
const IDENTIFY_RETRY_DELAYS_MS = [0, 5_000, 15_000];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The constants IDENTIFY_MAX_ATTEMPTS and IDENTIFY_RETRY_DELAYS_MS are tightly coupled, but this relationship is not enforced. A future change to one without updating the other could lead to runtime errors. Additionally, IDENTIFY_RETRY_DELAYS_MS[0] is unused in the retry logic, which can be confusing.

The suggested change derives the max attempts from the delays array and defines only the actual retry delays. This makes the configuration more robust.

Note: Applying this suggestion will require updating the identifyPeer function to use IDENTIFY_RETRY_DELAYS_MS[attempt - 1] for getting the delay, and IDENTIFY_RETRY_DELAYS_MS[attempt] for logging the next retry.

Suggested change
/** Maximum number of identify attempts per peer */
const IDENTIFY_MAX_ATTEMPTS = 3;
/** Retry delays for identify attempts (ms). First attempt is immediate. */
const IDENTIFY_RETRY_DELAYS_MS = [0, 5_000, 15_000];
/** Retry delays for identify attempts (ms). The length of the array determines the number of retries after the initial one. */
const IDENTIFY_RETRY_DELAYS_MS = [5_000, 15_000];
/** Maximum number of identify attempts per peer (1 initial + number of retries). */
const IDENTIFY_MAX_ATTEMPTS = 1 + IDENTIFY_RETRY_DELAYS_MS.length;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — deriving IDENTIFY_MAX_ATTEMPTS from the array eliminates the coupling. Will apply.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6eaa94e138

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


// remove the ping and status timer for the peer
this.connectedPeers.delete(peerIdStr);
this.identifyInProgress.delete(peerIdStr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve identify lock until retry loop actually exits

Clearing identifyInProgress on disconnect allows a new identify loop to start before the previous retry coroutine has finished sleeping. If a peer disconnects and reconnects within the 5s/15s backoff window, onStatus can launch a second loop while the old one resumes after its timeout, so two identify retries run concurrently for the same peer and defeat the deduplication this change is trying to enforce.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid race condition in theory. In practice, the disconnect handler in onPeerDisconnected removes the peer from peers entirely, so a reconnect creates a fresh peer entry with identifyInProgress=false. The old coroutine would resume and hit peers.get(peerId) returning undefined → early exit. But I agree it's fragile — I'll remove the identifyInProgress cleanup from disconnect and let the coroutine's finally block handle it instead, which is more robust.

- Add retry logic (3 attempts with [0s, 5s, 15s] backoff) for transient
  identify failures (EOF, stream closing errors)
- Add peer:identify event listener as safety net for identify-push from
  remote peers
- Use generation-based Map for identify deduplication to prevent stale
  retry loops from interfering with newer ones on disconnect/reconnect
- Add AbortController to cancel in-flight identify retries on shutdown
- Skip retry on non-retryable errors (public key missing, peer mismatch)
- Add 5 new tests: status-gated identify, no re-identify, retry on
  transient error, dedupe, non-retryable bailout, disconnect/reconnect
  race
…ct race

- Derive IDENTIFY_MAX_ATTEMPTS from IDENTIFY_RETRY_DELAYS_MS length
  to avoid tightly-coupled constants (Gemini review feedback)
- Remove identifyInProgress cleanup from disconnect handler to prevent
  reconnect race with sleeping retry coroutine (Codex review feedback)
  The .finally() block handles cleanup via generation check instead.
wemeetagain pushed a commit that referenced this pull request Feb 24, 2026
…8955)

## Motivation

PR #8890 (libp2p v3) suffers from ~22% Unknown peer rate (vs ~4% on
v2/unstable). Root cause: libp2p v3 enforces per-protocol stream limits
(`identify: maxOutboundStreams=1`). When repeated STATUS messages
trigger overlapping `identify()` calls for the same peer, v3 throws
`TooManyOutboundProtocolStreamsError` which cascades into massive EOF
failures (~5000 identify errors/2h on feat1 vs ~287 on unstable).

## Description

Minimal fix — no retries, no backoff, no spray:

- **Single in-flight identify per peer**: `identifyInProgress` map keyed
by `PeerIdStr → connection.id`. Before calling `identify()`, checks if
there's already one in-flight for the same connection. If so, skips.
- **Event-driven fallback**: Listens to `peer:identify` events from
libp2p (fired on successful identify or identify-push). Updates
`agentVersion/agentClient` even if our explicit `identify()` failed
earlier.
- **Reconnect race safety**: Uses `connection.id` as epoch token. After
`await identify()`, verifies the in-flight key still matches before
writing results — a reconnect during the await clears the old entry, so
stale results are discarded.
- **Cleanup on disconnect**: Removes in-flight tracking when peer
disconnects.

## Changes

- `packages/beacon-node/src/network/peers/peerManager.ts`: Added
`identifyInProgress` map, `onPeerIdentify` event handler, dedup guard in
`onStatus`, stale-result guard in `identifyPeer()`, cleanup in
disconnect handler
- `packages/beacon-node/test/e2e/network/peers/peerManager.test.ts`: 4
new tests (dedup, reconnect race, event-driven fallback, existing flow
preserved)

## Evidence

Loki log comparison (2h window):
- **feat1 (v3)**: ~5000 identify errors — 3794 EOF, 863
EOF-while-reading, 283 missing public key, 26 too-many-outbound-streams
- **unstable (v2)**: ~287 identify errors — 143 unexpected-end, 82
timeouts

```
# The overlapping call pattern (before fix):
STATUS #1 → identify() in-flight
STATUS #2 → identify() overlaps → TooManyOutboundProtocolStreamsError → EOF cascade

# After fix:
STATUS #1 → identify() in-flight, tracked in identifyInProgress
STATUS #2 → sees in-flight marker, skips
peer:identify event → updates agentVersion as safety net
```

> Note: This is a replacement for the retry-based approach in #8954.
That PR added retry machinery which masked the root cause rather than
preventing overlapping calls.

## AI Disclosure

This PR was authored with AI assistance (Claude Opus 4.6 via OpenClaw).
All code was reviewed and tested by the AI agent.

Co-authored-by: lodekeeper <lodekeeper@users.noreply.github.com>
@nflaig

nflaig commented Feb 24, 2026

Copy link
Copy Markdown
Member

closing, the minimal approach has been merged

@nflaig nflaig closed this Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants