Skip to content

Add getNodeVersionV2 endpoint#568

Merged
nflaig merged 19 commits intoethereum:masterfrom
eth2353:add-v2-node-version-endpoint
Feb 11, 2026
Merged

Add getNodeVersionV2 endpoint#568
nflaig merged 19 commits intoethereum:masterfrom
eth2353:add-v2-node-version-endpoint

Conversation

@eth2353
Copy link
Contributor

@eth2353 eth2353 commented Dec 22, 2025

Adds a /eth/v2/node/version endpoint for structured version information that also includes information about the attached EL client.

Changes:

The main motivation for this change is for validator clients to have access to information about the EL client attached to a beacon node. Right now, validator clients cannot see "past" the beacon node easily and are therefore blind to what kind of EL client is validating the execution payload. By exposing this information, multi-node validator clients can make safer decisions, e.g. they can require 3 different EL clients to declare the execution payload as valid before they attest to its validity.

@eth2353 eth2353 force-pushed the add-v2-node-version-endpoint branch from 6fe82bf to e0ea5c8 Compare December 28, 2025 14:22
@eth2353 eth2353 marked this pull request as ready for review December 28, 2025 14:23
@rolfyone
Copy link
Contributor

Similar to griffiti watermarks, it's reasonable to expect that there are nodes that either can't or choose not to pass back version specifics..... It's also seen sometimes in these watermarks that only one client (typically the CL) is declared (harder to tell if its unknown or no space).
I agree with the idea of the endpoint but we probably need to be very permissive in the structures that some or all of the information here isn't available, which will mean a lot more optionality in the output - I'm not sure if that dilutes what you're looking for?

@eth2353
Copy link
Contributor Author

eth2353 commented Jan 19, 2026

I consider this to be quite different from graffiti data since that is public, whereas the Beacon API is not generally exposed publicly. I don't really see an issue with passing back precise version information here. On the current V1 endpoint most clients already include everything that is proposed in this PR for the CL side, just in a less structured "User-agent" way:

Grandine/2.0.1-39e7dc37/x86_64-linux

Lighthouse/v8.0.0-e3ee7fe/x86_64-linux

Lodestar/v1.37.0/eaf5bc9

Nimbus/v25.12.0-ce4689-stateofus

teku/v25.11.0/linux-x86_64/-eclipseadoptium-openjdk64bitservervm-java-21 (no commit hash)


I can see the beacon node not having EL-side information for some reason (e.g. because the EL client is not responding to the beacon node's Engine API requests). In such cases we could either not include the "execution_client" field in the return data (and make that entire field optional), or explicitly define an UNKNOWN object that could be returned. I'd prefer to do the latter to help indicate that the field should be populated and not just skipped over.

For the usecase I'm looking for, just the client names would suffice, e.g. this node is running Lodestar+Reth. But since most clients already include much more in their current user-agent, I decided to just reuse the Engine API schema.

Copy link
Member

@nflaig nflaig left a comment

Choose a reason for hiding this comment

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

Overall looks good to me, this was pretty straight forward to implement ChainSafe/lodestar#8772 for us.

@eth2353
Copy link
Contributor Author

eth2353 commented Feb 3, 2026

I've added the following changes just now:

  • replaced github.com/.../blob/main/... with permalinks
  • updated the CHANGES.MD file deprecating the v1 endpoint and adding the v2
  • defined expected behavior for the situation where the client information is unknown (e.g. when no exchange of this information has occurred between the BN and EL client yet). In this case, the client code XX and client name Unknown should be used, with the remaining fields (version, commit) returned as empty strings.

I left the conversations regarding the "unknown" behavior unresolved for now in case anyone else wants to chime in on that.

nflaig
nflaig previously approved these changes Feb 3, 2026
@michaelsproul
Copy link
Contributor

LGTM, happy with "unknown" to signal an EL that is unresponsive or doesn't support the version endpoint

@mehdi-aouadi
Copy link
Contributor

mehdi-aouadi commented Feb 6, 2026

I think using "magic strings" like "Unknown", "XX" and empty strings could be problematic because it forces the consumer to know and handle them (they could consider them valid values since non standard). I'd rather prefer making them nullable or optional.
I can't find any example of nullable fields and idk if there's a reason for that. Making them all optional isn't ideal neither imo (we could end up with an empty response).
Ideally I'd suggest making some fields required like the code and version (minimal identity) and make the other fields nullable or optional. Then regarding the get method, the beacon_node must know itself so it should be required but the execution_client could become nullable or optional. If any required field in the execution_client isn't available then the whole object is null/empty and if any required field of the beacon_node isn't available we should respond with an error.
Example:

ClientVersionV1:
      type: object
      # minimal identity required
      required: [code, version] 
      properties:
        code:
          # required, a client must have a code
          type: string
          example: "LH"
        version:
          # required, a client must have a version
          type: string
          example: "v8.0.1"
        name:
          type: string
          nullable: true
          example: "Lighthouse"
        commit:
          type: string
          nullable: true
          example: "ced49dd2"

And the get method response:

responses:
    "200":
      description: Request successful
      content:
        application/json:
          schema:
            title: GetVersionV2Response
            type: object
            required: [data]
            properties:
              data:
                type: object
                required: [beacon_node]
                properties:
                  beacon_node:
                    # the beacon node always exists and has a valid identity
                    $ref: '../../beacon-node-oapi.yaml#/components/schemas/ClientVersionV1'
                  execution_client:
                    # the execution client is an optional dependency and is not required
                    $ref: '../../beacon-node-oapi.yaml#/components/schemas/ClientVersionV1'

TLDR;

  • All info available: 200
  • Required fields for the CL available but other non required fields are not: 200 with other fields set to null
  • Required fields for the CL available but other required fields for the EL are not: 200 with execution_client set to null entirely
  • Required fields for the CL unavailable: Error

@nflaig
Copy link
Member

nflaig commented Feb 6, 2026

I can't find any example of nullable fields and idk if there's a reason for that.

generally we don't want that because the response can't be ssz encoded if a field is nullable or optional but for this endpoint it doesn't matter, I would be fine with allowing null, it's better than making fields optional

there is one example for this

enr:
oneOf:
- type: "null"
- $ref: "./p2p.yaml#/ENR"

it doesn't use nullable: true which seems to be deprecated since openapi 3.1, see #409

Required fields for the CL unavailable: Error

this api should never return an error, I don't see why the CL wouldn't be able to return it's own client info

@mehdi-aouadi
Copy link
Contributor

mehdi-aouadi commented Feb 6, 2026

#409

Thanks for the clarifications.
Then the nullable values should be defined as: type: ["string", "null"] for the primitive types and

execution_client:
 oneOf:
  - $ref: '../../beacon-node-oapi.yaml#/components/schemas/ClientVersionV1'
  - type: "null"

for objects.

By error I meant the already defined 500 error could be enough (if the API isn't able to respond for any reason)

@rolfyone
Copy link
Contributor

rolfyone commented Feb 7, 2026

generally the field would just not be marked required, so if its null its not included in the output...

@rolfyone
Copy link
Contributor

rolfyone commented Feb 7, 2026

I can't find any example of nullable fields and idk if there's a reason for that. Making them all optional isn't ideal neither imo (we could end up with an empty response).

not required would be the normal in the beacon-api - we generally don't pass back 'null' fields, i'd far prefer a field thats not required and just dont pass back the field rather than having 'null' or having strings like unknown, but if it was for execution specifically i'd probably be less worried about saying 'unknown' if a non required field is an issue for some reason.

@nflaig
Copy link
Member

nflaig commented Feb 7, 2026

I am also fine with making fields optional if that's what everyone prefers to do

@eth2353
Copy link
Contributor Author

eth2353 commented Feb 9, 2026

I think using "magic strings" like "Unknown", "XX" and empty strings could be problematic

Agreed, I don't like these too much either.

Ideally I'd suggest making some [ClientVersionV1] fields required like the code and version (minimal identity) and make the other fields nullable or optional.

I am against doing this. Right now, ClientVersionV1 follows exactly the same schema as the version information that is exchanged over the Engine API. I don't see a good reason to deviate from it and complicate things here.

not required vs null

Based on the discussion above I'm leaning towards making the entire execution_client field optional instead of nullable.


I've reverted the "unknown" / "XX" magic strings and gone for the optional execution_client in my latest push. I also added response examples for both cases.

Let me know if there are any more concerns.

@nflaig
Copy link
Member

nflaig commented Feb 9, 2026

I am against doing this. Right now, ClientVersionV1 follows exactly the same schema as the version information that is exchanged over the Engine API. I don't see a good reason to deviate from it and complicate things here.

agreed, they are not optional on the engine api, I don't see a good reason why we would change that here, checking our existing type schema in Lodestar, all fields are required

changes look good to me, just need to resolve merge conflicts

Copy link
Contributor

@mehdi-aouadi mehdi-aouadi left a comment

Choose a reason for hiding this comment

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

LGTM
Just a nit

james-prysm
james-prysm previously approved these changes Feb 9, 2026
Copy link
Contributor

@james-prysm james-prysm left a comment

Choose a reason for hiding this comment

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

seems good to us too, good discussion on null vs optional always good to clearly define those

@eth2353 eth2353 dismissed stale reviews from james-prysm and nflaig via 1caecee February 10, 2026 11:56
nflaig
nflaig previously approved these changes Feb 10, 2026
Copy link
Member

@nflaig nflaig left a comment

Choose a reason for hiding this comment

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

looks good, even though I don't like making execution_client an array but for the sake of aligning it with the engine api format I am fine either way

@mehdi-aouadi
Copy link
Contributor

looks good, even though I don't like making execution_client an array but for the sake of aligning it with the engine api format I am fine either way

I'd rather prioritise the consumer and a clean abstraction over rigid adherence to an underlying implementation detail. The api should not be just a wrapper around an internal RPC call imo.
Plus this would force every consumer to handle a list.
One way to solve this is to let the beacon node decide which EL should be considered active/primary if it's connected to multiple ones.
If we want to use all the data from the engine API, it would maybe be better to yet add another field:

beacon_node:
        $ref: '...'
execution_client: # the primary/active client
        $ref: '../../beacon-node-oapi.yaml#/components/schemas/ClientVersionV1'
execution_client_pool: # the raw list of all clients
        type: array
        items:
          $ref: '../../beacon-node-oapi.yaml#/components/schemas/ClientVersionV1'

nflaig pushed a commit to ChainSafe/lodestar that referenced this pull request Feb 10, 2026
## Description

Updates the `getNodeVersionV2` endpoint to match the latest [beacon-APIs
spec (PR #568)](ethereum/beacon-APIs#568), which
changed `execution_client` from a single `ClientVersionV1` to an
**array** of `ClientVersionV1`.

This supports multiplexed EL connections where
`engine_getClientVersionV1` may return multiple entries.

### Changes

- `NodeVersionV2.executionClient` → `ClientVersion[]` (optional array)
- `IExecutionEngine.clientVersion` → `clientVersions` (stores full array
from Engine API)
- Graffiti uses `clientVersions?.[0]` for the default graffiti string
- Test data updated to match new array type

### Spec Reference

From `apis/node/version.v2.yaml`:
```yaml
execution_client:
  type: array
  items:
    $ref: '../../beacon-node-oapi.yaml#/components/schemas/ClientVersionV1'
```

> The `execution_client` field is an array because the Engine API's
`engine_getClientVersionV1` may return multiple entries when using
multiplexed connections.

---
*This PR was authored with AI assistance (Claude/Lodekeeper 🌟)*

---------

Co-authored-by: lodekeeper <lodekeeper@users.noreply.github.com>
@eth2353
Copy link
Contributor Author

eth2353 commented Feb 10, 2026

One way to solve this is to let the beacon node decide which EL should be considered active/primary if it's connected to multiple ones.

The thing is, the beacon node may not necessarily know. If something like this EL multiplexer is used, the beacon node only sees one EL endpoint but would (presumably) receive an array with multiple values back from the Engine API call. Which of the multiplexed EL clients is/are actually used to determine whether a payload is valid in subsequent API calls is not quite clear from this response.

I think it is still valuable to return a list with multiple values if multiple EL clients are somehow used by a single beacon node. Though for my usecase (Vero) this kind of response (multiple values) will simply be unsupported and raise an error.

Plus this would force every consumer to handle a list.

Yes but I don't see that as that big of a downside, the list of values is the more correct information in case there are multiple EL clients connected. How the consumer decides to handle that is up to them.

@mehdi-aouadi
Copy link
Contributor

The thing is, the beacon node may not necessarily know. If something like this EL multiplexer is used, the beacon node only sees one EL endpoint but would (presumably) receive an array with multiple values back from the Engine API call. Which of the multiplexed EL clients is/are actually used to determine whether a payload is valid in subsequent API calls is not quite clear from this response.

Since the multiplexer use case isn't standard, the BN should use deterministic heuristic if it's completely blind: the first from the list is the primary/active one for example. Decided by the BN and not the consumer.

I think it is still valuable to return a list with multiple values if multiple EL clients are somehow used by a single beacon node. Though for my usecase (Vero) this kind of response (multiple values) will simply be unsupported and raise an error.

I agree. That's why I suggested a "hybrid" approach:

  • Simple users might consume the execution_client and neglect the rest
  • More complexe ones could check the execution_client_pool too if needed

Yes but I don't see that as that big of a downside, the list of values is the more correct information in case there are multiple EL clients connected. How the consumer decides to handle that is up to them.

If a consumer is always interested in a single EL, it should, ideally, not be forced to handle a list imo.

Anyway, this is not a strong blocker for me

@eth2353
Copy link
Contributor Author

eth2353 commented Feb 10, 2026

the BN should use deterministic heuristic if it's completely blind: the first from the list is the primary/active one for example. Decided by the BN and not the consumer.

In case of the external multiplexer, the beacon node has no say over which client is used as primary/active, it doesn't know – the external multiplexer decides which EL node(s) processes each Engine API request. The multiplexer logic may be trivial (picking the first healthy node) but can also be much more complex (e.g. require 2 out of 3 EL clients to agree on payload validity).

The above is why I consider the hybrid approach interesting but insufficient to capture the full range of possibilities (from the point of view of behavior during consensus bugs which Beacon API consumers like Vero would be interested in).


I believe we may have reached the point where there is no single correct answer what should be returned here.

There are many possible options once you consider a beacon node is connected to multiple EL clients and they depend on how the beacon node interacts with those multiple EL clients, whether that's through a separate multiplexer or some internal logic. Trying to capture the full nuance of that in this Beacon API endpoint seems difficult to me.

Since the multiplexer use case isn't standard

The overlap between people running EL multiplexers and people interested in consuming this getNodeVersionV2 Beacon API endpoint is probably near zero. The standard is to run a client pair - one beacon node and one execution client.

Therefore I propose we refocus on that standard in this PR and simplify by simply returning a single object for the execution_client field. (edit: PR with this specific change)

If there's enough appetite in the future to work out the nuances for EL multiplexers, we can iterate on a v3 endpoint then but I think there's a good chance nobody will ever need that.

@nflaig
Copy link
Member

nflaig commented Feb 10, 2026

Therefore I propose we refocus on that standard in this PR and simplify by simply returning a single object for the execution_client field. (edit: PR with this specific change)

this makes sense also when looking at how current client implementations work

Client Calls API Uses Mux handling Auto graffiti
Lighthouse [0] len≠1 → skip watermark {EL}{commit}LH{commit} (adaptive)
Teku [0] Not handled {EL}{commit}TK{commit} (adaptive)
Lodestar [0] Not handled {EL}{commit}LS{commit} (fixed 12)
Grandine [0] len≠1 → skip watermark {EL}{commit}GR{commit} + full version
Prysm N/A N/A None
Nimbus N/A N/A None

all clients only use the first entry [0] of the engine_getClientVersionV1 response

@mehdi-aouadi
Copy link
Contributor

Therefore I propose we refocus on that standard in this PR and simplify by simply returning a single object for the execution_client field. (edit: PR with this specific change)

I agree with this approach. I initially approved that version of the PR.
As @nflaig mentioned, all the clients consume the first object of the list anyway so it makes sense.

lodekeeper added a commit to lodekeeper/lodestar that referenced this pull request Feb 11, 2026
The beacon-APIs spec PR ethereum/beacon-APIs#568 changed execution_client
from an array to a single optional ClientVersionV1 object.

If the Engine API returns multiple values, the beacon node should return
only the first one for consistent behavior across clients.

Changes:
- NodeVersionV2.executionClient: ClientVersion[] -> ClientVersion
- Updated implementation to return single object instead of array
- Updated test data accordingly
nflaig pushed a commit to ChainSafe/lodestar that referenced this pull request Feb 11, 2026
…ent (#8891)

## Description

Aligns `getNodeVersionV2` with the latest spec change in
[ethereum/beacon-APIs#568](ethereum/beacon-APIs#568).

The spec [reverted `execution_client` from an array to a single optional
object](eth2353/beacon-APIs@938aa33d) — if the
Engine API returns multiple values, the beacon node should return only
the first one for consistent behavior across clients.

### Changes
- `NodeVersionV2.executionClient`: `ClientVersion[]` → `ClientVersion`
(optional singular)
- Updated implementation to return single object instead of wrapping in
array
- Updated test data

### Type of change
- [x] Bug fix (non-breaking)

### Checklist
- [x] Types compile (`pnpm build` passes)
- [x] Unit tests pass (`pnpm --filter @lodestar/api test:unit` —
611/611)

_This PR was created with AI assistance (Lodekeeper 🌟)_

Co-authored-by: lodekeeper <lodekeeper@users.noreply.github.com>
Co-authored-by: Nico Flaig <nflaig@protonmail.com>
@nflaig nflaig merged commit d355842 into ethereum:master Feb 11, 2026
3 checks passed
nflaig added a commit to ChainSafe/lodestar that referenced this pull request Feb 12, 2026
See ethereum/beacon-APIs#568

---------

Co-authored-by: Lodekeeper <258435968+lodekeeper@users.noreply.github.com>
Co-authored-by: lodekeeper <lodekeeper@users.noreply.github.com>
Co-authored-by: NC <17676176+ensi321@users.noreply.github.com>
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.

6 participants