cl, engineapi: encode client versions in default block graffiti - #22303
Merged
Merged
Conversation
lystopad
requested review from
domiwei,
mh0lt,
sudeepdino008 and
yperbasis
as code owners
July 7, 2026 18:18
lystopad
enabled auto-merge
July 7, 2026 18:29
Contributor
There was a problem hiding this comment.
Pull request overview
Updates Caplin’s default block graffiti to follow the Engine API client-identification standard so proposed blocks can be attributed to both the execution (EL) and consensus (CL) clients, and wires Caplin to query engine_getClientVersionV1 to obtain the EL identification (with caching).
Changes:
- Adds
GetClientVersionV1to Caplin’sExecutionEngineinterface and implements it across real/mocked engines. - Introduces shared helpers (
NewClientVersionV1/LocalClientVersionV1) to constructClientVersionV1consistently. - Replaces Caplin’s default graffiti generation with standard
<EL code><EL commit><CL code><CL commit>encoding and adds targeted tests.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| execution/engineapi/engine_types/jsonrpc.go | Adds helpers to construct ClientVersionV1 (including local node version). |
| execution/engineapi/engine_api_methods.go | Simplifies GetClientVersionV1 handler to reuse the shared local helper. |
| cl/phase1/execution_client/interface.go | Extends the CL↔EL interface with GetClientVersionV1. |
| cl/phase1/execution_client/execution_engine_mock.go | Updates gomock generation to include GetClientVersionV1. |
| cl/phase1/execution_client/execution_client_engine.go | Plumbs GetClientVersionV1 through the engine-backed execution client. |
| cl/phase1/execution_client/execution_client_direct.go | Implements GetClientVersionV1 for in-process Erigon (direct mode). |
| cl/beacon/handler/handler.go | Adds an atomic cached pointer for EL client version used in default graffiti. |
| cl/beacon/handler/block_production.go | Implements standard default-graffiti encoding and EL version lookup/caching. |
| cl/beacon/handler/block_production_graffiti_test.go | Adds tests covering encoding, fallbacks, and caching behavior. |
| cl/spectest/consensus_tests/fork_choice.go | Updates spectest engine stub to satisfy the new interface method. |
| cl/phase1/stages/gloas_payload_test.go | Updates test execution engine stub to satisfy the new interface method. |
Files not reviewed (1)
- cl/phase1/execution_client/execution_engine_mock.go: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+99
to
+105
| func (a *ApiHandler) defaultGraffiti(ctx context.Context) common.Hash { | ||
| graffiti := caplinClientCode + graffitiCommitPrefix(version.GitCommit) | ||
| if el := a.executionClientVersion(ctx); el != nil { | ||
| graffiti = el.Code + graffitiCommitPrefix(el.Commit) + graffiti | ||
| } | ||
| return graffitiFromString(graffiti) | ||
| } |
Comment on lines
+136
to
+139
| ctx, cancel := context.WithTimeout(ctx, time.Second) | ||
| defer cancel() | ||
| caplin := engine_types.NewClientVersionV1(caplinClientCode, caplinClientName, a.version, version.GitCommit) | ||
| versions, err := a.engine.GetClientVersionV1(ctx, &caplin) |
Comment on lines
+185
to
+187
| // NewClientVersionV1 builds a ClientVersionV1 from a git commit hash, truncating | ||
| // it to the leading 4 bytes as required by | ||
| // https://github.com/ethereum/execution-apis/blob/main/src/engine/identification.md |
When a validator proposes a block without specifying graffiti, Caplin now fills it with the client-version graffiti standard instead of the literal "Caplin" string, so client-diversity tooling can attribute the block to its execution and consensus clients: <EL code><EL commit><CN><CL commit> e.g. EGa53eCNa53e Caplin uses the reserved consensus client code CN. The execution client's code and commit are obtained via engine_getClientVersionV1, which is added to the ExecutionEngine interface; the result is cached so steady-state block production stays off the engine API. When the execution client does not support the method, the graffiti degrades to the consensus client identifier only. User-specified graffiti is unaffected. Standard: https://github.com/ethereum/execution-apis/blob/main/src/engine/identification.md
executionClientVersion only cached a successful engine_getClientVersionV1 response, so an execution client that does not implement the method (or returns an empty list) was re-queried on every block proposal, incurring the engine API round-trip and its 1s timeout each time. Memoize the unavailable outcome via a sentinel so steady-state block production stays off the engine API in that case too.
…version Address review follow-ups on the default-graffiti client version lookup: - executionClientVersion memoized the negative result on any engine error, so a transient failure (including the 1s context timeout) would disable EL attribution until restart. Only cache the unavailable outcome when the method is genuinely unsupported: a JSON-RPC method-not-found (-32601) error or an empty version list. Other errors are treated as transient and retried. - NewClientVersionV1 now strips an existing "0x" prefix from the commit before re-prefixing, so a hex-prefixed input no longer yields "0x0x...".
On a cold cache, multiple concurrent block-production requests could each call engine_getClientVersionV1 before the first result was stored, a small burst of redundant engine calls at startup/reconnect. Collapse concurrent first-time fetches through a singleflight.Group with a double-check inside the flight, so only one engine call is in flight while the atomic-pointer cache and the transient-vs-unsupported handling are unchanged.
… proposal path Address review follow-ups on the default-graffiti client version lookup: - Move the engine_getClientVersionV1 fetch off the block-production critical path. A cold cache no longer blocks the proposal for up to the 1s engine timeout (which risked missed slots when the EL is slow/unreachable); instead the first proposal falls back to consensus-only graffiti and a single background fetch populates the cache for later proposals. A one-shot atomic guard replaces the singleflight group and still ensures only one in-flight fetch, while transient errors stay uncached and are retried. - Clamp the execution client code to the 2 bytes the graffiti standard reserves so a non-conforming EL code cannot misalign the encoding. - Add the missing space in the GetClientVersionV1 request log message. - Correct the NewClientVersionV1 doc comment to note the all-zero fallback for a missing or too-short commit hash.
lystopad
force-pushed
the
feature/lystopad/caplin-graffiti-client-version
branch
from
July 8, 2026 08:20
0c1bffe to
9121d16
Compare
yperbasis
approved these changes
Jul 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
When a validator proposes a block without specifying graffiti, Caplin now fills the graffiti with the client-version encoding from the client identification standard instead of the literal
"Caplin"string:This lets client-diversity tooling attribute proposed blocks to both their execution and consensus clients. Today an Erigon + Caplin node leaves no execution-client fingerprint in its blocks.
Why
Execution-layer client diversity is largely unmeasurable from block data. The standard addresses this by having the consensus client embed the execution and consensus client codes and their commit prefixes in the default graffiti. Caplin previously emitted only the literal
"Caplin", contributing no execution-client signal.How
GetClientVersionV1to theExecutionEngineinterface; Caplin obtains the execution client's code and commit viaengine_getClientVersionV1. The result is cached, so steady-state block production performs no extra engine API calls.CN.CN<commit>).engine_types.NewClientVersionV1/LocalClientVersionV1, now shared by the engine API handler and the in-process execution client.Dependency
Caplin's consensus client code
CNis registered in ethereum/execution-apis#844. This change can merge independently: the encoding works before registration (consumers must accept any two-letter code), and attribution tooling recognizesCNonce that PR lands.Testing
cl/beacon/handler/block_production_graffiti_test.go: encoding, execution-client-unavailable fallback, no-engine fallback, and cache behavior (the engine is queried once across multiple proposals).make lint,go vet, and the affected package tests pass.