From bbe5c262fc0114fd77495de7bb627b541f883f1b Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Wed, 13 May 2026 15:20:00 +0200 Subject: [PATCH 1/5] Add GET /eth/v1/debug/node/peers/scores Introduces a new debug endpoint that returns the consensus client's current per-peer scoring snapshot. Each entry includes the client- native blended score, the per-subsystem score components the client chooses to expose, the most recent score-affecting action (if any), and the most recent disconnect (if any). The schema is intentionally permissive about what each client surfaces, because the visibility eth2 clients have into their own scoring varies widely: - `components` is a flexible map; `gossipsub` is optional because some clients (notably Nimbus) have no application-layer access to the underlying libp2p scores. - `last_action` and `last_disconnect` are both optional - gossipsub- driven disconnects in particular often bypass the client's reason- capture path. - `score_range` is required so consumers can normalize across clients whose native score ranges differ wildly: [-100, +100] for Lighthouse / Lodestar / Grandine, [-10, +20] for Teku, [0, 1000] for Nimbus, [-100, +1] for Prysm. `PeerScoreReason` and `PeerDisconnectReason` are controlled vocabularies that group the common cross-client causes; the original client-side string is preserved in `native_reason` so consumers can distinguish e.g. multiple `rpc_*` flavors that map to the same controlled code. Prior art motivating this proposal: - Lighthouse `GET /lighthouse/peers` - Lodestar `GET /eth/v1/lodestar/lodestar_peer_score_stats` - Teku `GET /teku/v1/nodes/peer_scores` - Prysm internal `ScoreInfo` proto + the WIP REST endpoint on `OffchainLabs/prysm:peer-scores-ui` --- CHANGES.md | 1 + apis/debug/peer_scores.yaml | 55 +++++++ beacon-node-oapi.yaml | 2 + types/p2p.yaml | 301 ++++++++++++++++++++++++++++++++++++ 4 files changed, 359 insertions(+) create mode 100644 apis/debug/peer_scores.yaml diff --git a/CHANGES.md b/CHANGES.md index 3d3d946e..49f63522 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ There are likely to be descriptions etc outside of the list below, but new query | Endpoint | [Lighthouse](https://github.com/sigp/lighthouse) | [Lodestar](https://github.com/ChainSafe/lodestar) | [Nimbus](https://github.com/status-im/nimbus-eth2) | [Prysm](https://github.com/prysmaticlabs/prysm) | [Teku](https://github.com/ConsenSys/teku) | |---------------------------------------------------------------------------------------------------------------------|--------------------------------------------------|---------------------------------------------------|----------------------------------------------------|-------------------------------------------------|-------------------------------------------| +| `GET /eth/v1/debug/node/peers/scores` added | | | | | | | [#552](https://github.com/ethereum/beacon-APIs/pull/552) `GET /eth/v1/validator/execution_payload_bid/{slot}/{builder_index}` added | | | | | | | [#552](https://github.com/ethereum/beacon-APIs/pull/552) `GET /eth/v1/validator/payload_attestation_data/{slot}` added | | | | | | | [#552](https://github.com/ethereum/beacon-APIs/pull/552) `POST /eth/v1/validator/duties/ptc/{epoch}` added | | | | | | diff --git a/apis/debug/peer_scores.yaml b/apis/debug/peer_scores.yaml new file mode 100644 index 00000000..8ad12e06 --- /dev/null +++ b/apis/debug/peer_scores.yaml @@ -0,0 +1,55 @@ +get: + operationId: getDebugPeerScores + summary: Get per-peer scoring details (debug) + description: | + Retrieves the consensus client's current per-peer scoring snapshot. Returns + one entry per known peer (connected or recently disconnected) with the + client-native blended score, the per-subsystem score components the client + chooses to expose, the most recent score-affecting action (if any), and the + most recent disconnect (if any). + + This endpoint is intentionally permissive about what each client surfaces: + + - `components` is a flexible map. A client SHOULD include `gossipsub` when + it has visibility into the underlying libp2p-gossipsub score, but MAY + omit it when the application layer has no access to libp2p internals + (e.g. nim-libp2p does not currently expose per-peer gossip scores to the + eth2 application). + - `last_action` and `last_disconnect` MAY be omitted for a peer if no such + event has been observed. + - `score_range` is REQUIRED so consumers can normalize across clients with + wildly different internal score scales (e.g. `[-100, +100]` for + Lighthouse / Lodestar / Grandine, `[-10, +20]` for Teku, + `[0, 1000]` for Nimbus, `[-100, +1]` for Prysm). + + Reason codes (`last_action.reason` and `last_disconnect.reason`) are drawn + from a controlled vocabulary that captures the most common cross-client + downscore and disconnect causes. The original client-side string is + preserved in the matching `native_reason` field so consumers can + distinguish e.g. multiple `rpc_*` flavors that map to the same controlled + code. + tags: + - Debug + responses: + "200": + description: Request successful + content: + application/json: + schema: + title: GetDebugPeerScoresResponse + type: object + required: [generated_at, data] + properties: + generated_at: + description: | + Server time (in milliseconds since the unix epoch) at which + the snapshot was produced. Clients consuming the endpoint via + polling can use this to compute the freshness of the data. + $ref: "../../beacon-node-oapi.yaml#/components/schemas/Uint64" + example: 1715600000000 + data: + type: array + items: + $ref: "../../beacon-node-oapi.yaml#/components/schemas/PeerScore" + "500": + $ref: "../../beacon-node-oapi.yaml#/components/responses/InternalError" diff --git a/beacon-node-oapi.yaml b/beacon-node-oapi.yaml index b30df420..f65b1bc0 100644 --- a/beacon-node-oapi.yaml +++ b/beacon-node-oapi.yaml @@ -155,6 +155,8 @@ paths: $ref: './apis/debug/heads.v2.yaml' /eth/v1/debug/fork_choice: $ref: './apis/debug/fork_choice.yaml' + /eth/v1/debug/node/peers/scores: + $ref: './apis/debug/peer_scores.yaml' /eth/v1/node/identity: $ref: "./apis/node/identity.yaml" diff --git a/types/p2p.yaml b/types/p2p.yaml index 62fe0e50..7f24092a 100644 --- a/types/p2p.yaml +++ b/types/p2p.yaml @@ -80,3 +80,304 @@ Multiaddr: type: string description: "[Read more](https://docs.libp2p.io/reference/glossary/#multiaddr)" example: "/ip4/7.7.7.7/tcp/4242/p2p/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N" + +PeerScore: + type: object + description: | + Per-peer scoring snapshot. The `score` field is in the client-native + range advertised in `score_range`; consumers that need cross-client + comparable values should normalize against that range. Components + are an optional flexible map - clients vary on which sub-scores they + can expose, but at minimum SHOULD include `reputation` when their + scoring model has a notion of application-level reputation. + required: [peer_id, state, score, score_range, components] + properties: + peer_id: + $ref: "./p2p.yaml#/PeerId" + state: + $ref: "./p2p.yaml#/PeerConnectionState" + direction: + description: | + Direction of the connection, when known. MAY be omitted for + peers that have been disconnected long enough that the client + no longer tracks the original direction. + $ref: "./p2p.yaml#/PeerConnectionDirection" + agent_version: + type: string + description: | + The peer's libp2p agent version string, as obtained from the + libp2p identify protocol. MAY be omitted if not yet observed. + example: "Lighthouse/v8.1.3-66919c2/aarch64-linux" + client_kind: + type: string + description: | + Optional pre-resolved consensus-client family of the peer, when + the implementation can infer it (typically from `agent_version`). + Saves consumers from regex-matching the agent string. Common + values include `lighthouse`, `lodestar`, `nimbus`, `prysm`, + `teku`, `grandine`, `caplin`. Implementations MAY omit this + field or use values outside this list. + example: "lighthouse" + score: + type: number + format: double + description: | + Client-native blended score. The exact meaning depends on the + implementation - see `score_range` for the boundary thresholds. + For libp2p-gossipsub-blended models (Lighthouse, Lodestar, + Grandine), this is the value the client uses for disconnect + decisions. + example: -17.4828 + score_range: + $ref: "./p2p.yaml#/PeerScoreRange" + components: + $ref: "./p2p.yaml#/PeerScoreComponents" + last_action: + description: | + Most recent score-affecting event observed for this peer (if any). + $ref: "./p2p.yaml#/PeerScoreAction" + last_disconnect: + description: | + Most recent disconnect observed for this peer (if any). MAY be + omitted even for disconnected peers when the disconnect was + driven by libp2p internals that the eth2 application layer does + not have visibility into (typical for gossipsub-score-driven + disconnects). + $ref: "./p2p.yaml#/PeerDisconnectAction" + +PeerScoreRange: + type: object + description: | + Boundary metadata for the client's score range, so consumers can + interpret the raw `score` value without hardcoding per-client + constants. All values are in the same client-native units as + `PeerScore.score`. + required: [min, max, disconnect_threshold, ban_threshold] + properties: + min: + type: number + format: double + description: "Minimum score the client will report. Scores at or near this value indicate the worst possible reputation." + example: -100.0 + max: + type: number + format: double + description: "Maximum score the client will report." + example: 100.0 + disconnect_threshold: + type: number + format: double + description: "Score at or below which the client will normally disconnect from a peer." + example: -20.0 + ban_threshold: + type: number + format: double + description: "Score at or below which the client will normally ban a peer (refusing reconnections for some implementation-defined period)." + example: -50.0 + +PeerScoreComponents: + type: object + description: | + Per-subsystem score breakdown. All fields are OPTIONAL. The set of + keys a client populates is implementation-specific - the spec + enumerates well-known ones below, but implementations MAY add + additional keys for their own internal scorers. + properties: + gossipsub: + type: number + format: double + description: | + Raw libp2p-gossipsub score. Note this value uses libp2p's own + scale, not necessarily the same as `score`. MAY be omitted when + the application layer has no access to libp2p internals (e.g. + Nimbus). + example: -38.6417 + reputation: + type: number + format: double + description: | + Application-level reputation score driven by the client's own + downscore actions (RPC errors, invalid gossip, sync failures, + etc.). The scale here matches the client-native range in + `score_range`. + example: 0.0 + bad_responses: + type: number + format: double + description: | + Component contributed by a bad-responses scorer (Prysm-style). + example: 0.0 + peer_status: + type: number + format: double + description: | + Component contributed by a peer-status scorer (Prysm-style). + example: 1.0 + block_provider: + type: number + format: double + description: | + Component contributed by a block-provider scorer (Prysm-style, + rewarding peers that supply blocks during sync). + behaviour_penalty: + type: number + format: double + description: | + Running libp2p-gossipsub P7 behaviour-penalty counter. + example: 0.0 + ip_colocation: + type: number + format: double + description: | + Running libp2p-gossipsub P6 ip-colocation-factor counter. + +PeerScoreAction: + type: object + description: | + A discrete score-affecting event recorded against a peer. + required: [reason, native_reason] + properties: + reason: + $ref: "./p2p.yaml#/PeerScoreReason" + native_reason: + type: string + description: | + The original client-side reason tag, preserved for fidelity. + Useful when multiple native tags collapse into a single + controlled `reason` code. + example: "rpc_io_error" + delta: + type: number + format: double + description: | + Change applied to `score` as a result of this event (negative for + downscores, positive for rewards). MAY be omitted when the event + was a state transition (e.g. ban) without a precise score + contribution. + example: -1.0 + topic: + type: string + description: | + Gossipsub topic associated with the event, if applicable. + example: "/eth2/4a26c58b/beacon_block/ssz_snappy" + seconds_ago: + description: "Seconds elapsed since the event was observed, clamped to a non-negative value." + $ref: "./primitive.yaml#/Uint64" + example: 12 + +PeerDisconnectAction: + type: object + description: | + The most recent disconnect event observed for a peer. + required: [reason, native_reason, direction] + properties: + reason: + $ref: "./p2p.yaml#/PeerDisconnectReason" + native_reason: + type: string + description: "The client-native disconnect reason, preserved for fidelity." + example: "BadScore" + goodbye_code: + description: | + The numeric Goodbye code exchanged on the wire, when one was + sent or received. Codes 0-127 are reserved by the consensus + p2p-interface spec; 128+ are implementation-defined extensions + (e.g. 128=UnableToVerifyNetwork, 129=TooManyPeers, 250=BadScore, + 251=Banned, 252=BannedIP). + $ref: "./primitive.yaml#/Uint64" + example: 250 + direction: + type: string + enum: ["sent", "received"] + description: | + Whether the disconnect was initiated by this client (`sent`) or + the peer (`received`). + seconds_ago: + $ref: "./primitive.yaml#/Uint64" + example: 120 + +PeerScoreReason: + type: string + description: | + Controlled vocabulary for a score-affecting event. Implementations + MUST emit one of these values. New codes will be added through the + spec process; consumers SHOULD tolerate unknown values and fall back + to the `category` derivable from the code's prefix. + enum: + - rpc_invalid_request + - rpc_invalid_response_ssz + - rpc_response_timeout + - rpc_server_error + - rpc_resource_unavailable + - rpc_rate_limited + - rpc_unsupported_protocol + - rpc_incomplete_stream + - rpc_dial_error + - rpc_io_error + - rpc_stream_timeout + - rpc_invalid_data + - rpc_handler_rejected + - rpc_disconnected + - rpc_internal_error + - rpc_bad_blocks_by_range + - rpc_bad_blocks_by_root + - rpc_bad_blobs + - rpc_bad_data_columns + - rpc_other + - gossip_invalid_block + - gossip_invalid_attestation + - gossip_invalid_sync_message + - gossip_invalid_blob_sidecar + - gossip_invalid_data_column_sidecar + - gossip_invalid_slashing + - gossip_invalid_voluntary_exit + - gossip_invalid_bls_change + - gossip_other + - sync_bad_batch + - sync_chain_invalid + - sync_lookup_failed + - sync_max_processing_attempts + - sync_other + - status_bad_fork_digest + - status_invalid_finalized_root + - status_unviable_fork + - status_low_head + - status_stale + - status_other + - colocation + - behaviour_penalty + - das_bad_column_intersection + - gossipsub_low + - bad_responses_accumulated + - peer_status_failed + - reward_good_response + - reward_good_status + - reward_block_provider + - unknown + +PeerDisconnectReason: + type: string + description: | + Controlled vocabulary for a disconnect cause. See `goodbye_code` for + the numeric wire code when one was exchanged. + enum: + - client_shutdown + - too_many_peers + - duplicate_connection + - irrelevant_network + - unable_to_verify_network + - unviable_fork + - bad_score + - banned + - banned_ip + - rate_limited + - fault + - unresponsive + - no_status_received + - invalid_handshake + - dial_error + - io_error + - stream_timeout + - connection_lost + - other + - unknown From 6d0a00f53096cdcb7775b5edd76119b49655851c Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Wed, 13 May 2026 15:20:42 +0200 Subject: [PATCH 2/5] fix(peer_scores): use /eth/v1/debug/node/peer_scores path Matches the existing flat snake_case URL convention used by /eth/v1/node/peer_count and the file name peer_scores.yaml. Prior revision used /eth/v1/debug/node/peers/scores which mixed subdirectory-style with the rest of the API. --- CHANGES.md | 2 +- beacon-node-oapi.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 49f63522..7e28a76d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ There are likely to be descriptions etc outside of the list below, but new query | Endpoint | [Lighthouse](https://github.com/sigp/lighthouse) | [Lodestar](https://github.com/ChainSafe/lodestar) | [Nimbus](https://github.com/status-im/nimbus-eth2) | [Prysm](https://github.com/prysmaticlabs/prysm) | [Teku](https://github.com/ConsenSys/teku) | |---------------------------------------------------------------------------------------------------------------------|--------------------------------------------------|---------------------------------------------------|----------------------------------------------------|-------------------------------------------------|-------------------------------------------| -| `GET /eth/v1/debug/node/peers/scores` added | | | | | | +| `GET /eth/v1/debug/node/peer_scores` added | | | | | | | [#552](https://github.com/ethereum/beacon-APIs/pull/552) `GET /eth/v1/validator/execution_payload_bid/{slot}/{builder_index}` added | | | | | | | [#552](https://github.com/ethereum/beacon-APIs/pull/552) `GET /eth/v1/validator/payload_attestation_data/{slot}` added | | | | | | | [#552](https://github.com/ethereum/beacon-APIs/pull/552) `POST /eth/v1/validator/duties/ptc/{epoch}` added | | | | | | diff --git a/beacon-node-oapi.yaml b/beacon-node-oapi.yaml index f65b1bc0..de882dec 100644 --- a/beacon-node-oapi.yaml +++ b/beacon-node-oapi.yaml @@ -155,7 +155,7 @@ paths: $ref: './apis/debug/heads.v2.yaml' /eth/v1/debug/fork_choice: $ref: './apis/debug/fork_choice.yaml' - /eth/v1/debug/node/peers/scores: + /eth/v1/debug/node/peer_scores: $ref: './apis/debug/peer_scores.yaml' /eth/v1/node/identity: From cff06ce61eb4da9a2ab98281867df926f07cc25e Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Thu, 14 May 2026 09:32:28 +0200 Subject: [PATCH 3/5] refactor(peer_scores): extend /eth/v1/node/peers instead of new endpoint Drop the separate /eth/v1/debug/node/peer_scores endpoint and the PeerScore/PeerScoreRange/PeerScoreComponents/PeerScoreAction/ PeerDisconnectAction object hierarchy in favour of four optional fields on the existing Peer schema: agent_version, score, disconnect_reason, downscore_reasons. Trim PeerScoreReason from 51 to 15 controlled values and PeerDisconnectReason from 20 to 8, keeping the cross-client realistic union rather than the full taxonomy. Implementations that compute finer-grained internal tags are expected to map them onto the closest listed value. Strictly additive change to /eth/v1/node/peers and /eth/v1/node/peers/ {peer_id}: all new fields are optional so existing consumers are unaffected. --- CHANGES.md | 2 +- apis/debug/peer_scores.yaml | 55 ------ beacon-node-oapi.yaml | 2 - types/p2p.yaml | 322 ++++++------------------------------ 4 files changed, 50 insertions(+), 331 deletions(-) delete mode 100644 apis/debug/peer_scores.yaml diff --git a/CHANGES.md b/CHANGES.md index 7e28a76d..a7254db7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ There are likely to be descriptions etc outside of the list below, but new query | Endpoint | [Lighthouse](https://github.com/sigp/lighthouse) | [Lodestar](https://github.com/ChainSafe/lodestar) | [Nimbus](https://github.com/status-im/nimbus-eth2) | [Prysm](https://github.com/prysmaticlabs/prysm) | [Teku](https://github.com/ConsenSys/teku) | |---------------------------------------------------------------------------------------------------------------------|--------------------------------------------------|---------------------------------------------------|----------------------------------------------------|-------------------------------------------------|-------------------------------------------| -| `GET /eth/v1/debug/node/peer_scores` added | | | | | | +| `GET /eth/v1/node/peers` and `GET /eth/v1/node/peers/{peer_id}` extended with optional `agent_version`, `score`, `disconnect_reason`, `downscore_reasons` fields | | | | | | | [#552](https://github.com/ethereum/beacon-APIs/pull/552) `GET /eth/v1/validator/execution_payload_bid/{slot}/{builder_index}` added | | | | | | | [#552](https://github.com/ethereum/beacon-APIs/pull/552) `GET /eth/v1/validator/payload_attestation_data/{slot}` added | | | | | | | [#552](https://github.com/ethereum/beacon-APIs/pull/552) `POST /eth/v1/validator/duties/ptc/{epoch}` added | | | | | | diff --git a/apis/debug/peer_scores.yaml b/apis/debug/peer_scores.yaml deleted file mode 100644 index 8ad12e06..00000000 --- a/apis/debug/peer_scores.yaml +++ /dev/null @@ -1,55 +0,0 @@ -get: - operationId: getDebugPeerScores - summary: Get per-peer scoring details (debug) - description: | - Retrieves the consensus client's current per-peer scoring snapshot. Returns - one entry per known peer (connected or recently disconnected) with the - client-native blended score, the per-subsystem score components the client - chooses to expose, the most recent score-affecting action (if any), and the - most recent disconnect (if any). - - This endpoint is intentionally permissive about what each client surfaces: - - - `components` is a flexible map. A client SHOULD include `gossipsub` when - it has visibility into the underlying libp2p-gossipsub score, but MAY - omit it when the application layer has no access to libp2p internals - (e.g. nim-libp2p does not currently expose per-peer gossip scores to the - eth2 application). - - `last_action` and `last_disconnect` MAY be omitted for a peer if no such - event has been observed. - - `score_range` is REQUIRED so consumers can normalize across clients with - wildly different internal score scales (e.g. `[-100, +100]` for - Lighthouse / Lodestar / Grandine, `[-10, +20]` for Teku, - `[0, 1000]` for Nimbus, `[-100, +1]` for Prysm). - - Reason codes (`last_action.reason` and `last_disconnect.reason`) are drawn - from a controlled vocabulary that captures the most common cross-client - downscore and disconnect causes. The original client-side string is - preserved in the matching `native_reason` field so consumers can - distinguish e.g. multiple `rpc_*` flavors that map to the same controlled - code. - tags: - - Debug - responses: - "200": - description: Request successful - content: - application/json: - schema: - title: GetDebugPeerScoresResponse - type: object - required: [generated_at, data] - properties: - generated_at: - description: | - Server time (in milliseconds since the unix epoch) at which - the snapshot was produced. Clients consuming the endpoint via - polling can use this to compute the freshness of the data. - $ref: "../../beacon-node-oapi.yaml#/components/schemas/Uint64" - example: 1715600000000 - data: - type: array - items: - $ref: "../../beacon-node-oapi.yaml#/components/schemas/PeerScore" - "500": - $ref: "../../beacon-node-oapi.yaml#/components/responses/InternalError" diff --git a/beacon-node-oapi.yaml b/beacon-node-oapi.yaml index de882dec..b30df420 100644 --- a/beacon-node-oapi.yaml +++ b/beacon-node-oapi.yaml @@ -155,8 +155,6 @@ paths: $ref: './apis/debug/heads.v2.yaml' /eth/v1/debug/fork_choice: $ref: './apis/debug/fork_choice.yaml' - /eth/v1/debug/node/peer_scores: - $ref: './apis/debug/peer_scores.yaml' /eth/v1/node/identity: $ref: "./apis/node/identity.yaml" diff --git a/types/p2p.yaml b/types/p2p.yaml index 7f24092a..4ffe160a 100644 --- a/types/p2p.yaml +++ b/types/p2p.yaml @@ -57,6 +57,40 @@ Peer: $ref: "./p2p.yaml#/PeerConnectionState" direction: $ref: "./p2p.yaml#/PeerConnectionDirection" + agent_version: + type: string + description: | + The peer's libp2p agent version string (from the libp2p identify + protocol). OPTIONAL; clients MAY omit if not yet observed. + example: "Lighthouse/v8.1.3-66919c2/aarch64-linux" + score: + type: number + format: double + description: | + Client-native peer score. OPTIONAL. The scale and meaning is + implementation-defined - consumers SHOULD treat it as a relative + signal within a single client, not directly comparable across + clients. Lower values indicate worse standing. Clients that do + not maintain a per-peer score MAY omit this field. + example: -17.4828 + disconnect_reason: + description: | + Reason the client last disconnected from this peer. OPTIONAL. + MUST only be populated when `state` is `disconnected` or + `disconnecting`. Clients MAY omit when no specific reason is + known. + $ref: "./p2p.yaml#/PeerDisconnectReason" + downscore_reasons: + type: array + description: | + Score-affecting events observed for this peer within the + client's recent-history window (implementation-defined, + typically the last few minutes). OPTIONAL. The most recent + event SHOULD appear first. Clients MAY omit if no events + within the window or if event tracking is not implemented. + items: + $ref: "./p2p.yaml#/PeerScoreReason" + example: ["rpc_bad_blocks_by_range", "rpc_rate_limited"] PeerConnectionState: type: string @@ -81,303 +115,45 @@ Multiaddr: description: "[Read more](https://docs.libp2p.io/reference/glossary/#multiaddr)" example: "/ip4/7.7.7.7/tcp/4242/p2p/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N" -PeerScore: - type: object - description: | - Per-peer scoring snapshot. The `score` field is in the client-native - range advertised in `score_range`; consumers that need cross-client - comparable values should normalize against that range. Components - are an optional flexible map - clients vary on which sub-scores they - can expose, but at minimum SHOULD include `reputation` when their - scoring model has a notion of application-level reputation. - required: [peer_id, state, score, score_range, components] - properties: - peer_id: - $ref: "./p2p.yaml#/PeerId" - state: - $ref: "./p2p.yaml#/PeerConnectionState" - direction: - description: | - Direction of the connection, when known. MAY be omitted for - peers that have been disconnected long enough that the client - no longer tracks the original direction. - $ref: "./p2p.yaml#/PeerConnectionDirection" - agent_version: - type: string - description: | - The peer's libp2p agent version string, as obtained from the - libp2p identify protocol. MAY be omitted if not yet observed. - example: "Lighthouse/v8.1.3-66919c2/aarch64-linux" - client_kind: - type: string - description: | - Optional pre-resolved consensus-client family of the peer, when - the implementation can infer it (typically from `agent_version`). - Saves consumers from regex-matching the agent string. Common - values include `lighthouse`, `lodestar`, `nimbus`, `prysm`, - `teku`, `grandine`, `caplin`. Implementations MAY omit this - field or use values outside this list. - example: "lighthouse" - score: - type: number - format: double - description: | - Client-native blended score. The exact meaning depends on the - implementation - see `score_range` for the boundary thresholds. - For libp2p-gossipsub-blended models (Lighthouse, Lodestar, - Grandine), this is the value the client uses for disconnect - decisions. - example: -17.4828 - score_range: - $ref: "./p2p.yaml#/PeerScoreRange" - components: - $ref: "./p2p.yaml#/PeerScoreComponents" - last_action: - description: | - Most recent score-affecting event observed for this peer (if any). - $ref: "./p2p.yaml#/PeerScoreAction" - last_disconnect: - description: | - Most recent disconnect observed for this peer (if any). MAY be - omitted even for disconnected peers when the disconnect was - driven by libp2p internals that the eth2 application layer does - not have visibility into (typical for gossipsub-score-driven - disconnects). - $ref: "./p2p.yaml#/PeerDisconnectAction" - -PeerScoreRange: - type: object - description: | - Boundary metadata for the client's score range, so consumers can - interpret the raw `score` value without hardcoding per-client - constants. All values are in the same client-native units as - `PeerScore.score`. - required: [min, max, disconnect_threshold, ban_threshold] - properties: - min: - type: number - format: double - description: "Minimum score the client will report. Scores at or near this value indicate the worst possible reputation." - example: -100.0 - max: - type: number - format: double - description: "Maximum score the client will report." - example: 100.0 - disconnect_threshold: - type: number - format: double - description: "Score at or below which the client will normally disconnect from a peer." - example: -20.0 - ban_threshold: - type: number - format: double - description: "Score at or below which the client will normally ban a peer (refusing reconnections for some implementation-defined period)." - example: -50.0 - -PeerScoreComponents: - type: object - description: | - Per-subsystem score breakdown. All fields are OPTIONAL. The set of - keys a client populates is implementation-specific - the spec - enumerates well-known ones below, but implementations MAY add - additional keys for their own internal scorers. - properties: - gossipsub: - type: number - format: double - description: | - Raw libp2p-gossipsub score. Note this value uses libp2p's own - scale, not necessarily the same as `score`. MAY be omitted when - the application layer has no access to libp2p internals (e.g. - Nimbus). - example: -38.6417 - reputation: - type: number - format: double - description: | - Application-level reputation score driven by the client's own - downscore actions (RPC errors, invalid gossip, sync failures, - etc.). The scale here matches the client-native range in - `score_range`. - example: 0.0 - bad_responses: - type: number - format: double - description: | - Component contributed by a bad-responses scorer (Prysm-style). - example: 0.0 - peer_status: - type: number - format: double - description: | - Component contributed by a peer-status scorer (Prysm-style). - example: 1.0 - block_provider: - type: number - format: double - description: | - Component contributed by a block-provider scorer (Prysm-style, - rewarding peers that supply blocks during sync). - behaviour_penalty: - type: number - format: double - description: | - Running libp2p-gossipsub P7 behaviour-penalty counter. - example: 0.0 - ip_colocation: - type: number - format: double - description: | - Running libp2p-gossipsub P6 ip-colocation-factor counter. - -PeerScoreAction: - type: object - description: | - A discrete score-affecting event recorded against a peer. - required: [reason, native_reason] - properties: - reason: - $ref: "./p2p.yaml#/PeerScoreReason" - native_reason: - type: string - description: | - The original client-side reason tag, preserved for fidelity. - Useful when multiple native tags collapse into a single - controlled `reason` code. - example: "rpc_io_error" - delta: - type: number - format: double - description: | - Change applied to `score` as a result of this event (negative for - downscores, positive for rewards). MAY be omitted when the event - was a state transition (e.g. ban) without a precise score - contribution. - example: -1.0 - topic: - type: string - description: | - Gossipsub topic associated with the event, if applicable. - example: "/eth2/4a26c58b/beacon_block/ssz_snappy" - seconds_ago: - description: "Seconds elapsed since the event was observed, clamped to a non-negative value." - $ref: "./primitive.yaml#/Uint64" - example: 12 - -PeerDisconnectAction: - type: object - description: | - The most recent disconnect event observed for a peer. - required: [reason, native_reason, direction] - properties: - reason: - $ref: "./p2p.yaml#/PeerDisconnectReason" - native_reason: - type: string - description: "The client-native disconnect reason, preserved for fidelity." - example: "BadScore" - goodbye_code: - description: | - The numeric Goodbye code exchanged on the wire, when one was - sent or received. Codes 0-127 are reserved by the consensus - p2p-interface spec; 128+ are implementation-defined extensions - (e.g. 128=UnableToVerifyNetwork, 129=TooManyPeers, 250=BadScore, - 251=Banned, 252=BannedIP). - $ref: "./primitive.yaml#/Uint64" - example: 250 - direction: - type: string - enum: ["sent", "received"] - description: | - Whether the disconnect was initiated by this client (`sent`) or - the peer (`received`). - seconds_ago: - $ref: "./primitive.yaml#/Uint64" - example: 120 - PeerScoreReason: type: string description: | - Controlled vocabulary for a score-affecting event. Implementations - MUST emit one of these values. New codes will be added through the - spec process; consumers SHOULD tolerate unknown values and fall back - to the `category` derivable from the code's prefix. + Controlled vocabulary for a score-affecting event recorded against + a peer. Implementations populating `Peer.downscore_reasons` MUST + emit values from this list. Consumers SHOULD tolerate unknown + values for forward compatibility. The intent is a small, + cross-client-meaningful set - clients with finer-grained internal + tags are expected to map them onto the closest value here. enum: - rpc_invalid_request - - rpc_invalid_response_ssz - - rpc_response_timeout - - rpc_server_error - - rpc_resource_unavailable + - rpc_invalid_response - rpc_rate_limited - - rpc_unsupported_protocol - - rpc_incomplete_stream - - rpc_dial_error + - rpc_timeout - rpc_io_error - - rpc_stream_timeout - - rpc_invalid_data - - rpc_handler_rejected - - rpc_disconnected - - rpc_internal_error - rpc_bad_blocks_by_range - rpc_bad_blocks_by_root - - rpc_bad_blobs - - rpc_bad_data_columns - - rpc_other - gossip_invalid_block - gossip_invalid_attestation - - gossip_invalid_sync_message - gossip_invalid_blob_sidecar - gossip_invalid_data_column_sidecar - - gossip_invalid_slashing - - gossip_invalid_voluntary_exit - - gossip_invalid_bls_change - - gossip_other - sync_bad_batch - - sync_chain_invalid - - sync_lookup_failed - - sync_max_processing_attempts - - sync_other - - status_bad_fork_digest - - status_invalid_finalized_root - status_unviable_fork - - status_low_head - - status_stale - - status_other - - colocation - behaviour_penalty - - das_bad_column_intersection - - gossipsub_low - - bad_responses_accumulated - - peer_status_failed - - reward_good_response - - reward_good_status - - reward_block_provider - unknown PeerDisconnectReason: type: string description: | - Controlled vocabulary for a disconnect cause. See `goodbye_code` for - the numeric wire code when one was exchanged. + Controlled vocabulary for the reason a client disconnected from a + peer. Implementations populating `Peer.disconnect_reason` MUST emit + values from this list. Consumers SHOULD tolerate unknown values for + forward compatibility. enum: - - client_shutdown - - too_many_peers - - duplicate_connection + - bad_score - irrelevant_network - - unable_to_verify_network - unviable_fork - - bad_score - - banned - - banned_ip + - too_many_peers - rate_limited - - fault - - unresponsive - - no_status_received - - invalid_handshake - - dial_error - io_error - - stream_timeout - - connection_lost - - other + - client_shutdown - unknown From 48e7fbfe22ca09e43462f2ba3a6a6616937760f0 Mon Sep 17 00:00:00 2001 From: Paul Harris Date: Fri, 26 Jun 2026 15:57:55 +1000 Subject: [PATCH 4/5] review feedback --- CHANGES.md | 1 - types/p2p.yaml | 33 +++++++++++---------------------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a421d082..d4efaabf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,6 @@ There are likely to be descriptions etc outside of the list below, but new query | Endpoint | [Lighthouse](https://github.com/sigp/lighthouse) | [Lodestar](https://github.com/ChainSafe/lodestar) | [Nimbus](https://github.com/status-im/nimbus-eth2) | [Prysm](https://github.com/prysmaticlabs/prysm) | [Teku](https://github.com/ConsenSys/teku) | |---------------------------------------------------------------------------------------------------------------------|--------------------------------------------------|---------------------------------------------------|----------------------------------------------------|-------------------------------------------------|-------------------------------------------| -| `GET /eth/v1/node/peers` and `GET /eth/v1/node/peers/{peer_id}` extended with optional `agent_version`, `score`, `disconnect_reason`, `downscore_reasons` fields | | | | | | | [#552](https://github.com/ethereum/beacon-APIs/pull/552) `GET /eth/v1/validator/execution_payload_bid/{slot}/{builder_index}` added | | | | | | | [#552](https://github.com/ethereum/beacon-APIs/pull/552) `GET /eth/v1/validator/payload_attestation_data/{slot}` added | | | | | | | [#552](https://github.com/ethereum/beacon-APIs/pull/552) `POST /eth/v1/validator/duties/ptc/{epoch}` added | | | | | | diff --git a/types/p2p.yaml b/types/p2p.yaml index 4ffe160a..6c45d9b1 100644 --- a/types/p2p.yaml +++ b/types/p2p.yaml @@ -67,27 +67,19 @@ Peer: type: number format: double description: | - Client-native peer score. OPTIONAL. The scale and meaning is - implementation-defined - consumers SHOULD treat it as a relative - signal within a single client, not directly comparable across - clients. Lower values indicate worse standing. Clients that do - not maintain a per-peer score MAY omit this field. + Client-native peer score. OPTIONAL. + This number is not consistent between clients, + but can be compared to other peers on the same client. example: -17.4828 disconnect_reason: description: | Reason the client last disconnected from this peer. OPTIONAL. - MUST only be populated when `state` is `disconnected` or - `disconnecting`. Clients MAY omit when no specific reason is - known. + This field MUST only be set if the connection status is disconnected or disconnecting. $ref: "./p2p.yaml#/PeerDisconnectReason" downscore_reasons: type: array description: | - Score-affecting events observed for this peer within the - client's recent-history window (implementation-defined, - typically the last few minutes). OPTIONAL. The most recent - event SHOULD appear first. Clients MAY omit if no events - within the window or if event tracking is not implemented. + Reasons that the client has been down scored in their current session. OPTIONAL. items: $ref: "./p2p.yaml#/PeerScoreReason" example: ["rpc_bad_blocks_by_range", "rpc_rate_limited"] @@ -118,13 +110,11 @@ Multiaddr: PeerScoreReason: type: string description: | - Controlled vocabulary for a score-affecting event recorded against - a peer. Implementations populating `Peer.downscore_reasons` MUST + Limited set of peer downscore reasons that may be returned. + Implementations populating `Peer.downscore_reasons` MUST emit values from this list. Consumers SHOULD tolerate unknown - values for forward compatibility. The intent is a small, - cross-client-meaningful set - clients with finer-grained internal - tags are expected to map them onto the closest value here. - enum: + values for forward compatibility. + example: - rpc_invalid_request - rpc_invalid_response - rpc_rate_limited @@ -144,11 +134,10 @@ PeerScoreReason: PeerDisconnectReason: type: string description: | - Controlled vocabulary for the reason a client disconnected from a - peer. Implementations populating `Peer.disconnect_reason` MUST emit + Implementations populating `Peer.disconnect_reason` MUST emit values from this list. Consumers SHOULD tolerate unknown values for forward compatibility. - enum: + example: - bad_score - irrelevant_network - unviable_fork From 06c5b4917a2e74e2bb8c71d6879ae80cc505a60a Mon Sep 17 00:00:00 2001 From: Paul Harris Date: Sat, 27 Jun 2026 11:28:39 +1000 Subject: [PATCH 5/5] clarify downscore reasons --- types/p2p.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/types/p2p.yaml b/types/p2p.yaml index 6c45d9b1..9ad13f78 100644 --- a/types/p2p.yaml +++ b/types/p2p.yaml @@ -80,6 +80,7 @@ Peer: type: array description: | Reasons that the client has been down scored in their current session. OPTIONAL. + SHOULD be a distinct set of downscare reasons. items: $ref: "./p2p.yaml#/PeerScoreReason" example: ["rpc_bad_blocks_by_range", "rpc_rate_limited"]