Skip to content

Implement eth/v1/beacon/light_client/updates - #11202

Open
akronim26 wants to merge 11 commits into
Consensys-Incorporated:masterfrom
akronim26:updates-by-range
Open

Implement eth/v1/beacon/light_client/updates#11202
akronim26 wants to merge 11 commits into
Consensys-Incorporated:masterfrom
akronim26:updates-by-range

Conversation

@akronim26

@akronim26 akronim26 commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

PR Description

This PR implements eth/v1/beacon/light_client/updates, which returned 501 until now. The PR:

  • serves the best updates for the requested period range from ChainDataProvider
  • caps count at MAX_REQUEST_LIGHT_CLIENT_UPDATES
  • adds LightClientUpdateWithContext, which pairs each update with the fork digest at its attested header slot
  • writes the SSZ response as response_chunk_len | context | payload chunks, as the spec expects
  • picks the JSON schema from the milestone at the attested header slot, so a range that crosses a fork is still returned correctly
  • removes LightClientUpdateResponse and its schema, which did not match the wire format
  • removes the TAG_EXPERIMENTAL tag and drops the 501, 503 and 204 responses, to match GetLightClientBootstrap

Fixed Issue(s)

Resolves a part of #4230

Documentation

  • I thought about documentation and added the doc-change-required label to this PR if updates are required.

Changelog

  • I thought about adding a changelog entry, and added one if I deemed necessary.

Note

Medium Risk
New production REST surface serves consensus light-client data with fork-specific encoding; incorrect SSZ or fork digest would break light clients, though behavior is covered by unit/metadata tests.

Overview
Implements GET /eth/v1/beacon/light_client/updates, which previously always returned 501. The handler now reads start_period and count, caps count at MAX_REQUEST_LIGHT_CLIENT_UPDATES (128), and returns the best updates from ChainDataProvider.getBestLightClientUpdates.

Each update is wrapped in LightClientUpdateWithContext (fork digest at the attested header slot + the LightClientUpdate). JSON responses use milestone-aware typing (version from attested slot); SSZ (application/octet-stream) emits repeated chunks of response_chunk_len | context | payload. OpenAPI drops the Experimental tag and 501, and documents a oneOf of fork-specific LightClientUpdate schemas (new Capella/Deneb/Electra/Gloas fixture schemas).

Removes LightClientUpdateResponse and its Altair schema wiring in favor of the spec wire format. Tests cover happy path, count capping, empty range, JSON/SSZ metadata, and handler wiring via DataProvider/ChainDataProvider.

Reviewed by Cursor Bugbot for commit 7a9df83. Bugbot is set up for automated code reviews on this repo. Configure here.

@rolfyone

rolfyone commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

claude review:

Review summary

The PR is in good shape. It correctly implements the spec wire format, and the approach to fork-aware schema selection (single ConditionalSchemaGetter covering all forks ≥ ALTAIR) is correct — though subtle.

Two things worth noting as positive changes that aren't immediately obvious:

  • The old SSZ serialization was broken. LightClientUpdateResponse.sszSerialize() on a Container3 with a variable field emits a 4-byte SSZ offset entry, producing [8B len][4B context][4B offset][NB payload] instead of the spec's [8B len][4B
    context][NB payload]. The new manual byte-writing is correct. The new test's responseChunk() helper validates this.
  • LightClientUpdateElectra.json referencing LightClientHeaderDeneb is intentional — Electra doesn't change the light client header format.

One actionable bug (finding 1): In ChainDataProvider.getBestLightClientUpdates, getGenesisStateData() is called before checking whether the update list is empty. This throws ChainDataUnavailableException for empty-range requests when genesis data
isn't yet available, returning 503 where 200+[] is expected. Fix: fetch updates first, early-return List.of() if empty, then call getGenesisStateData().

@akronim26

Copy link
Copy Markdown
Contributor Author

claude review:

Review summary

The PR is in good shape. It correctly implements the spec wire format, and the approach to fork-aware schema selection (single ConditionalSchemaGetter covering all forks ≥ ALTAIR) is correct — though subtle.

Two things worth noting as positive changes that aren't immediately obvious:

  • The old SSZ serialization was broken. LightClientUpdateResponse.sszSerialize() on a Container3 with a variable field emits a 4-byte SSZ offset entry, producing [8B len][4B context][4B offset][NB payload] instead of the spec's [8B len][4B
    context][NB payload]. The new manual byte-writing is correct. The new test's responseChunk() helper validates this.
  • LightClientUpdateElectra.json referencing LightClientHeaderDeneb is intentional — Electra doesn't change the light client header format.

One actionable bug (finding 1): In ChainDataProvider.getBestLightClientUpdates, getGenesisStateData() is called before checking whether the update list is empty. This throws ChainDataUnavailableException for empty-range requests when genesis data isn't yet available, returning 503 where 200+[] is expected. Fix: fetch updates first, early-return List.of() if empty, then call getGenesisStateData().

Fixed. getBestLightClientUpdates now returns an empty list, in case of no updates, before getting the genesis state data.

@rolfyone rolfyone 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.

LGTM

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