diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_beacon_light_client_updates.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_beacon_light_client_updates.json index 59914da7125..8b3cc081182 100644 --- a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_beacon_light_client_updates.json +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_beacon_light_client_updates.json @@ -1,9 +1,9 @@ { "get" : { - "tags" : [ "Beacon", "Experimental" ], + "tags" : [ "Beacon" ], "operationId" : "getLightClientUpdatesByRange", "summary" : "Get `LightClientUpdate` instances in a requested sync committee period range", - "description" : "Requests the [`LightClientUpdate`](https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/altair/light-client/sync-protocol.md#lightclientupdate) instances in the sync committee period range `[start_period, start_period + count)`, leading up to the current head sync committee period as selected by fork choice. Depending on the `Accept` header they can be returned either as JSON or SSZ-serialized bytes.", + "description" : "Requests the `LightClientUpdate` instances in the sync committee period range `[start_period, start_period + count)`, leading up to the current head sync committee period as selected by fork choice. Depending on the `Accept` header they can be returned either as JSON or SSZ-serialized bytes.", "parameters" : [ { "name" : "start_period", "required" : true, @@ -41,7 +41,19 @@ "enum" : [ "phase0", "altair", "bellatrix", "capella", "deneb", "electra", "fulu", "gloas", "heze" ] }, "data" : { - "$ref" : "#/components/schemas/LightClientUpdateAltair" + "title" : "LightClientUpdate", + "type" : "object", + "oneOf" : [ { + "$ref" : "#/components/schemas/LightClientUpdateAltair" + }, { + "$ref" : "#/components/schemas/LightClientUpdateCapella" + }, { + "$ref" : "#/components/schemas/LightClientUpdateDeneb" + }, { + "$ref" : "#/components/schemas/LightClientUpdateElectra" + }, { + "$ref" : "#/components/schemas/LightClientUpdateGloas" + } ] } } } @@ -65,16 +77,6 @@ } } }, - "501" : { - "description" : "Not implemented", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/HttpErrorResponse" - } - } - } - }, "400" : { "description" : "The request could not be processed, check the response for more information.", "content" : { diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/LightClientUpdateCapella.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/LightClientUpdateCapella.json new file mode 100644 index 00000000000..a59679bbf0a --- /dev/null +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/LightClientUpdateCapella.json @@ -0,0 +1,43 @@ +{ + "title" : "LightClientUpdateCapella", + "type" : "object", + "required" : [ "attested_header", "next_sync_committee", "next_sync_committee_branch", "finalized_header", "finality_branch", "sync_aggregate", "signature_slot" ], + "properties" : { + "attested_header" : { + "$ref" : "#/components/schemas/LightClientHeaderCapella" + }, + "next_sync_committee" : { + "$ref" : "#/components/schemas/SyncCommittee" + }, + "next_sync_committee_branch" : { + "type" : "array", + "items" : { + "type" : "string", + "description" : "Bytes32 hexadecimal", + "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "format" : "byte" + } + }, + "finalized_header" : { + "$ref" : "#/components/schemas/LightClientHeaderCapella" + }, + "finality_branch" : { + "type" : "array", + "items" : { + "type" : "string", + "description" : "Bytes32 hexadecimal", + "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "format" : "byte" + } + }, + "sync_aggregate" : { + "$ref" : "#/components/schemas/SyncAggregate" + }, + "signature_slot" : { + "type" : "string", + "description" : "unsigned 64 bit integer", + "example" : "1", + "format" : "uint64" + } + } +} \ No newline at end of file diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/LightClientUpdateDeneb.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/LightClientUpdateDeneb.json new file mode 100644 index 00000000000..d4735654d0b --- /dev/null +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/LightClientUpdateDeneb.json @@ -0,0 +1,43 @@ +{ + "title" : "LightClientUpdateDeneb", + "type" : "object", + "required" : [ "attested_header", "next_sync_committee", "next_sync_committee_branch", "finalized_header", "finality_branch", "sync_aggregate", "signature_slot" ], + "properties" : { + "attested_header" : { + "$ref" : "#/components/schemas/LightClientHeaderDeneb" + }, + "next_sync_committee" : { + "$ref" : "#/components/schemas/SyncCommittee" + }, + "next_sync_committee_branch" : { + "type" : "array", + "items" : { + "type" : "string", + "description" : "Bytes32 hexadecimal", + "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "format" : "byte" + } + }, + "finalized_header" : { + "$ref" : "#/components/schemas/LightClientHeaderDeneb" + }, + "finality_branch" : { + "type" : "array", + "items" : { + "type" : "string", + "description" : "Bytes32 hexadecimal", + "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "format" : "byte" + } + }, + "sync_aggregate" : { + "$ref" : "#/components/schemas/SyncAggregate" + }, + "signature_slot" : { + "type" : "string", + "description" : "unsigned 64 bit integer", + "example" : "1", + "format" : "uint64" + } + } +} \ No newline at end of file diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/LightClientUpdateElectra.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/LightClientUpdateElectra.json new file mode 100644 index 00000000000..d60fb673f98 --- /dev/null +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/LightClientUpdateElectra.json @@ -0,0 +1,43 @@ +{ + "title" : "LightClientUpdateElectra", + "type" : "object", + "required" : [ "attested_header", "next_sync_committee", "next_sync_committee_branch", "finalized_header", "finality_branch", "sync_aggregate", "signature_slot" ], + "properties" : { + "attested_header" : { + "$ref" : "#/components/schemas/LightClientHeaderDeneb" + }, + "next_sync_committee" : { + "$ref" : "#/components/schemas/SyncCommittee" + }, + "next_sync_committee_branch" : { + "type" : "array", + "items" : { + "type" : "string", + "description" : "Bytes32 hexadecimal", + "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "format" : "byte" + } + }, + "finalized_header" : { + "$ref" : "#/components/schemas/LightClientHeaderDeneb" + }, + "finality_branch" : { + "type" : "array", + "items" : { + "type" : "string", + "description" : "Bytes32 hexadecimal", + "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "format" : "byte" + } + }, + "sync_aggregate" : { + "$ref" : "#/components/schemas/SyncAggregate" + }, + "signature_slot" : { + "type" : "string", + "description" : "unsigned 64 bit integer", + "example" : "1", + "format" : "uint64" + } + } +} \ No newline at end of file diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/LightClientUpdateGloas.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/LightClientUpdateGloas.json new file mode 100644 index 00000000000..2e3bed8e621 --- /dev/null +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/LightClientUpdateGloas.json @@ -0,0 +1,43 @@ +{ + "title" : "LightClientUpdateGloas", + "type" : "object", + "required" : [ "attested_header", "next_sync_committee", "next_sync_committee_branch", "finalized_header", "finality_branch", "sync_aggregate", "signature_slot" ], + "properties" : { + "attested_header" : { + "$ref" : "#/components/schemas/LightClientHeaderGloas" + }, + "next_sync_committee" : { + "$ref" : "#/components/schemas/SyncCommittee" + }, + "next_sync_committee_branch" : { + "type" : "array", + "items" : { + "type" : "string", + "description" : "Bytes32 hexadecimal", + "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "format" : "byte" + } + }, + "finalized_header" : { + "$ref" : "#/components/schemas/LightClientHeaderGloas" + }, + "finality_branch" : { + "type" : "array", + "items" : { + "type" : "string", + "description" : "Bytes32 hexadecimal", + "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "format" : "byte" + } + }, + "sync_aggregate" : { + "$ref" : "#/components/schemas/SyncAggregate" + }, + "signature_slot" : { + "type" : "string", + "description" : "unsigned 64 bit integer", + "example" : "1", + "format" : "uint64" + } + } +} \ No newline at end of file diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/addon/LightClientRestApiBuilderAddon.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/addon/LightClientRestApiBuilderAddon.java index 9c61854f962..7de028ef592 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/addon/LightClientRestApiBuilderAddon.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/addon/LightClientRestApiBuilderAddon.java @@ -45,6 +45,6 @@ public boolean isEnabled() { public RestApiBuilder apply(final RestApiBuilder builder) { return builder .endpoint(new GetLightClientBootstrap(dataProvider, schemaCache)) - .endpoint(new GetLightClientUpdatesByRange(schemaCache)); + .endpoint(new GetLightClientUpdatesByRange(dataProvider, schemaCache)); } } diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/lightclient/GetLightClientUpdatesByRange.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/lightclient/GetLightClientUpdatesByRange.java index 9361bfb142d..a3fc304c951 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/lightclient/GetLightClientUpdatesByRange.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/lightclient/GetLightClientUpdatesByRange.java @@ -15,80 +15,121 @@ import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.COUNT_PARAMETER; import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.START_PERIOD_PARAMETER; +import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil.getMultipleSchemaDefinitionFromMilestone; import static tech.pegasys.teku.ethereum.json.types.EthereumTypes.MILESTONE_TYPE; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_BEACON; -import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_EXPERIMENTAL; +import static tech.pegasys.teku.spec.constants.NetworkConstants.MAX_REQUEST_LIGHT_CLIENT_UPDATES; import com.fasterxml.jackson.core.JsonProcessingException; +import java.nio.ByteOrder; import java.util.Collections; import java.util.List; +import org.apache.tuweni.bytes.Bytes; +import tech.pegasys.teku.api.ChainDataProvider; +import tech.pegasys.teku.api.DataProvider; +import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil; +import tech.pegasys.teku.infrastructure.bytes.Bytes4; import tech.pegasys.teku.infrastructure.json.types.SerializableTypeDefinition; import tech.pegasys.teku.infrastructure.restapi.endpoints.EndpointMetadata; import tech.pegasys.teku.infrastructure.restapi.endpoints.RestApiEndpoint; import tech.pegasys.teku.infrastructure.restapi.endpoints.RestApiRequest; -import tech.pegasys.teku.infrastructure.restapi.openapi.response.JsonResponseContentTypeDefinition; import tech.pegasys.teku.infrastructure.restapi.openapi.response.OctetStreamResponseContentTypeDefinition; import tech.pegasys.teku.infrastructure.restapi.openapi.response.ResponseContentTypeDefinition; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.SpecMilestone; import tech.pegasys.teku.spec.datastructures.lightclient.LightClientUpdate; -import tech.pegasys.teku.spec.datastructures.lightclient.LightClientUpdateResponse; -import tech.pegasys.teku.spec.datastructures.metadata.ObjectAndMetaData; +import tech.pegasys.teku.spec.datastructures.metadata.LightClientUpdateWithContext; import tech.pegasys.teku.spec.schemas.SchemaDefinitionCache; import tech.pegasys.teku.spec.schemas.SchemaDefinitionsAltair; public class GetLightClientUpdatesByRange extends RestApiEndpoint { public static final String ROUTE = "/eth/v1/beacon/light_client/updates"; - public GetLightClientUpdatesByRange(final SchemaDefinitionCache schemaDefinitionCache) { + private final ChainDataProvider chainDataProvider; + + public GetLightClientUpdatesByRange( + final DataProvider provider, final SchemaDefinitionCache schemaDefinitionCache) { + this(provider.getChainDataProvider(), schemaDefinitionCache); + } + + public GetLightClientUpdatesByRange( + final ChainDataProvider chainDataProvider, + final SchemaDefinitionCache schemaDefinitionCache) { super( EndpointMetadata.get(ROUTE) .operationId("getLightClientUpdatesByRange") .summary("Get `LightClientUpdate` instances in a requested sync committee period range") .description( - "Requests the [`LightClientUpdate`](https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/altair/light-client/sync-protocol.md#lightclientupdate) instances in the sync committee period range `[start_period, start_period + count)`, leading up to the current head sync committee period as selected by fork choice. Depending on the `Accept` header they can be returned either as JSON or SSZ-serialized bytes.") - .tags(TAG_BEACON, TAG_EXPERIMENTAL) + "Requests the `LightClientUpdate` instances in the sync committee period range `[start_period, start_period + count)`, leading up to the current head sync committee period as selected by fork choice. Depending on the `Accept` header they can be returned either as JSON or SSZ-serialized bytes.") + .tags(TAG_BEACON) .queryParamRequired(START_PERIOD_PARAMETER) .queryParamRequired(COUNT_PARAMETER) .response( SC_OK, "Request successful", - List.of(getJsonResponseType(schemaDefinitionCache), getSszResponseType())) + getJsonResponseType(schemaDefinitionCache), + getSszResponseType()) .withNotAcceptableResponse() - .withNotImplementedResponse() .build()); + this.chainDataProvider = chainDataProvider; } @Override public void handleRequest(final RestApiRequest request) throws JsonProcessingException { - request.respondError(501, "Not implemented"); + final UInt64 startPeriod = request.getQueryParameter(START_PERIOD_PARAMETER); + final int count = + request.getQueryParameter(COUNT_PARAMETER).min(MAX_REQUEST_LIGHT_CLIENT_UPDATES).intValue(); + + request.respondOk(chainDataProvider.getBestLightClientUpdates(startPeriod, count)); } - private static ResponseContentTypeDefinition>> - getJsonResponseType(final SchemaDefinitionCache schemaDefinitionCache) { + private static SerializableTypeDefinition> getJsonResponseType( + final SchemaDefinitionCache schemaDefinitionCache) { final SerializableTypeDefinition lightClientUpdateType = - SchemaDefinitionsAltair.required( - schemaDefinitionCache.getSchemaDefinition(SpecMilestone.ALTAIR)) - .getLightClientUpdateSchema() - .getJsonTypeDefinition(); + getMultipleSchemaDefinitionFromMilestone( + schemaDefinitionCache, + "LightClientUpdate", + List.of( + new MilestoneDependentTypesUtil.ConditionalSchemaGetter<>( + (update, milestone) -> + milestoneAtAttestedSlot(schemaDefinitionCache, update).equals(milestone), + SpecMilestone.ALTAIR, + schemaDefinitions -> + SchemaDefinitionsAltair.required(schemaDefinitions) + .getLightClientUpdateSchema()))); - final SerializableTypeDefinition> - lightClientUpdateObjectType = - SerializableTypeDefinition.>object() - .withField("version", MILESTONE_TYPE, ObjectAndMetaData::getMilestone) - .withField("data", lightClientUpdateType, ObjectAndMetaData::getData) - .build(); + return SerializableTypeDefinition.listOf( + SerializableTypeDefinition.object() + .withField( + "version", + MILESTONE_TYPE, + updateWithContext -> + milestoneAtAttestedSlot(schemaDefinitionCache, updateWithContext.update())) + .withField("data", lightClientUpdateType, LightClientUpdateWithContext::update) + .build()); + } - return new JsonResponseContentTypeDefinition<>( - SerializableTypeDefinition.listOf(lightClientUpdateObjectType)); + private static SpecMilestone milestoneAtAttestedSlot( + final SchemaDefinitionCache schemaDefinitionCache, final LightClientUpdate update) { + return schemaDefinitionCache.milestoneAtSlot(update.getAttestedHeader().getBeacon().getSlot()); } - private static ResponseContentTypeDefinition> + private static ResponseContentTypeDefinition> getSszResponseType() { - OctetStreamResponseContentTypeDefinition.OctetStreamSerializer> + OctetStreamResponseContentTypeDefinition.OctetStreamSerializer< + List> serializer = - (data, out) -> - data.stream().forEachOrdered(lcuResponse -> lcuResponse.sszSerialize(out)); + (data, out) -> { + for (final LightClientUpdateWithContext updateWithContext : data) { + final Bytes payload = updateWithContext.update().sszSerialize(); + out.write( + Bytes.ofUnsignedLong(Bytes4.SIZE + payload.size(), ByteOrder.LITTLE_ENDIAN) + .toArrayUnsafe()); + out.write(updateWithContext.context().getWrappedBytes().toArrayUnsafe()); + out.write(payload.toArrayUnsafe()); + } + }; return new OctetStreamResponseContentTypeDefinition<>(serializer, __ -> Collections.emptyMap()); } diff --git a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/lightclient/GetLightClientUpdatesByRangeTest.java b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/lightclient/GetLightClientUpdatesByRangeTest.java index f6c8fd16e05..58bd6886d49 100644 --- a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/lightclient/GetLightClientUpdatesByRangeTest.java +++ b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/lightclient/GetLightClientUpdatesByRangeTest.java @@ -14,47 +14,90 @@ package tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.lightclient; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.when; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_INTERNAL_SERVER_ERROR; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NOT_ACCEPTABLE; -import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NOT_IMPLEMENTED; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.getResponseSszFromMetadata; import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.getResponseStringFromMetadata; import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.verifyMetadataErrorResponse; +import static tech.pegasys.teku.spec.constants.NetworkConstants.MAX_REQUEST_LIGHT_CLIENT_UPDATES; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.io.Resources; import java.io.IOException; +import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.util.List; +import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import tech.pegasys.teku.beaconrestapi.AbstractMigratedBeaconHandlerTest; +import tech.pegasys.teku.beaconrestapi.AbstractMigratedBeaconHandlerWithChainDataProviderTest; +import tech.pegasys.teku.infrastructure.bytes.Bytes4; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.SpecMilestone; import tech.pegasys.teku.spec.TestSpecFactory; import tech.pegasys.teku.spec.datastructures.lightclient.LightClientUpdate; -import tech.pegasys.teku.spec.datastructures.lightclient.LightClientUpdateResponse; -import tech.pegasys.teku.spec.datastructures.metadata.ObjectAndMetaData; -import tech.pegasys.teku.spec.util.DataStructureUtil; +import tech.pegasys.teku.spec.datastructures.metadata.LightClientUpdateWithContext; -public class GetLightClientUpdatesByRangeTest extends AbstractMigratedBeaconHandlerTest { +public class GetLightClientUpdatesByRangeTest + extends AbstractMigratedBeaconHandlerWithChainDataProviderTest { @BeforeEach void setup() { - setHandler(new GetLightClientUpdatesByRange(schemaDefinitionCache)); - dataStructureUtil = new DataStructureUtil(TestSpecFactory.createMinimalAltair()); + setSpec(TestSpecFactory.createMinimalAltair()); + setHandler(new GetLightClientUpdatesByRange(chainDataProvider, schemaDefinitionCache)); request.setQueryParameter("start_period", "1"); request.setQueryParameter("count", "1"); } + @Test + void shouldReturnBestUpdates() throws Exception { + final List updates = + List.of(dataStructureUtil.randomLightClientUpdateWithContext(UInt64.ONE)); + when(chainDataProvider.getBestLightClientUpdates(UInt64.ONE, 1)).thenReturn(updates); + + handler.handleRequest(request); + + assertThat(request.getResponseCode()).isEqualTo(SC_OK); + assertThat(request.getResponseBody()).isEqualTo(updates); + } + + @Test + void shouldCapCountAtMaxRequestLightClientUpdates() throws Exception { + setHandler(new GetLightClientUpdatesByRange(chainDataProvider, schemaDefinitionCache)); + request.setQueryParameter("start_period", "1"); + request.setQueryParameter("count", "1000"); + when(chainDataProvider.getBestLightClientUpdates(UInt64.ONE, MAX_REQUEST_LIGHT_CLIENT_UPDATES)) + .thenReturn(List.of()); + + handler.handleRequest(request); + + assertThat(request.getResponseCode()).isEqualTo(SC_OK); + assertThat(request.getResponseBody()).isEqualTo(List.of()); + } + + @Test + void shouldReturnEmptyListWhenGenesisDataIsUnavailableAndRangeIsEmpty() throws Exception { + initialise(SpecMilestone.ALTAIR); + setHandler(new GetLightClientUpdatesByRange(chainDataProvider, schemaDefinitionCache)); + request.setQueryParameter("start_period", "1"); + request.setQueryParameter("count", "1"); + + handler.handleRequest(request); + + assertThat(request.getResponseCode()).isEqualTo(SC_OK); + assertThat(request.getResponseBody()).isEqualTo(List.of()); + } + @Test void metadata_shouldHandleJson200() throws IOException { - LightClientUpdate lightClientUpdate = dataStructureUtil.randomLightClientUpdate(UInt64.ONE); - ObjectAndMetaData responseData = - new ObjectAndMetaData<>(lightClientUpdate, SpecMilestone.ALTAIR, false, true, false); - List> response = List.of(responseData); + final LightClientUpdate lightClientUpdate = + dataStructureUtil.randomLightClientUpdate(UInt64.ONE); + final List response = + List.of( + new LightClientUpdateWithContext(dataStructureUtil.randomBytes4(), lightClientUpdate)); final String data = getResponseStringFromMetadata(handler, SC_OK, response); final String expected = @@ -67,14 +110,23 @@ void metadata_shouldHandleJson200() throws IOException { @Test void metadata_shouldHandleSsz200() throws IOException { - LightClientUpdateResponse responseData = - dataStructureUtil.randomLightClientUpdateResponse(UInt64.ONE); - List response = List.of(responseData); + final LightClientUpdateWithContext first = + dataStructureUtil.randomLightClientUpdateWithContext(UInt64.ONE); + final LightClientUpdateWithContext second = + dataStructureUtil.randomLightClientUpdateWithContext(UInt64.ONE); - final byte[] actual = getResponseSszFromMetadata(handler, SC_OK, response); - final byte[] expected = responseData.sszSerialize().toArray(); + final byte[] actual = getResponseSszFromMetadata(handler, SC_OK, List.of(first, second)); - assertThat(actual).isEqualTo(expected); + assertThat(Bytes.wrap(actual)) + .isEqualTo(Bytes.wrap(responseChunk(first), responseChunk(second))); + } + + private static Bytes responseChunk(final LightClientUpdateWithContext updateWithContext) { + final Bytes payload = updateWithContext.update().sszSerialize(); + return Bytes.wrap( + Bytes.ofUnsignedLong(Bytes4.SIZE + payload.size(), ByteOrder.LITTLE_ENDIAN), + updateWithContext.context().getWrappedBytes(), + payload); } @Test @@ -91,9 +143,4 @@ void metadata_shouldHandle406() throws JsonProcessingException { void metadata_shouldHandle500() throws JsonProcessingException { verifyMetadataErrorResponse(handler, SC_INTERNAL_SERVER_ERROR); } - - @Test - public void metadata_shouldHandle501() throws JsonProcessingException { - verifyMetadataErrorResponse(handler, SC_NOT_IMPLEMENTED); - } } diff --git a/data/provider/src/main/java/tech/pegasys/teku/api/ChainDataProvider.java b/data/provider/src/main/java/tech/pegasys/teku/api/ChainDataProvider.java index f60e7c11c0b..bd8a553d1e0 100644 --- a/data/provider/src/main/java/tech/pegasys/teku/api/ChainDataProvider.java +++ b/data/provider/src/main/java/tech/pegasys/teku/api/ChainDataProvider.java @@ -82,6 +82,7 @@ import tech.pegasys.teku.spec.datastructures.metadata.BlockAndMetaData; import tech.pegasys.teku.spec.datastructures.metadata.DataColumnSidecarsAndMetaData; import tech.pegasys.teku.spec.datastructures.metadata.ExecutionPayloadAndMetaData; +import tech.pegasys.teku.spec.datastructures.metadata.LightClientUpdateWithContext; import tech.pegasys.teku.spec.datastructures.metadata.ObjectAndMetaData; import tech.pegasys.teku.spec.datastructures.metadata.StateAndMetaData; import tech.pegasys.teku.spec.datastructures.operations.Attestation; @@ -168,9 +169,28 @@ public ChainDataProvider( this.lightClientUpdateStore = lightClientUpdateStore; } - public List getBestLightClientUpdates( + public List getBestLightClientUpdates( final UInt64 startPeriod, final int count) { - return lightClientUpdateStore.getBestUpdatesInRange(startPeriod, count); + final List updates = + lightClientUpdateStore.getBestUpdatesInRange(startPeriod, count); + + if (updates.isEmpty()) { + return List.of(); + } + + final Bytes32 genesisValidatorsRoot = getGenesisStateData().getGenesisValidatorsRoot(); + return updates.stream() + .map( + update -> + new LightClientUpdateWithContext( + spec.computeForkDigest( + genesisValidatorsRoot, spec.computeEpochAtSlot(attestedSlot(update))), + update)) + .toList(); + } + + private static UInt64 attestedSlot(final LightClientUpdate update) { + return update.getAttestedHeader().getBeacon().getSlot(); } public Optional getLatestLightClientFinalityUpdate() { diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/constants/NetworkConstants.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/constants/NetworkConstants.java index d11d1433b7f..453129a76b0 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/constants/NetworkConstants.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/constants/NetworkConstants.java @@ -22,4 +22,6 @@ public class NetworkConstants { // https://github.com/ethereum/consensus-specs/pull/3767 public static final int MAX_CONCURRENT_REQUESTS = 2; + + public static final int MAX_REQUEST_LIGHT_CLIENT_UPDATES = 128; } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/lightclient/LightClientUpdateResponse.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/lightclient/LightClientUpdateResponse.java deleted file mode 100644 index 14995686e76..00000000000 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/lightclient/LightClientUpdateResponse.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Consensys Software Inc., 2026 - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package tech.pegasys.teku.spec.datastructures.lightclient; - -import tech.pegasys.teku.infrastructure.ssz.containers.Container3; -import tech.pegasys.teku.infrastructure.ssz.primitive.SszBytes4; -import tech.pegasys.teku.infrastructure.ssz.primitive.SszUInt64; -import tech.pegasys.teku.infrastructure.ssz.tree.TreeNode; - -public class LightClientUpdateResponse - extends Container3 { - - public LightClientUpdateResponse( - final LightClientUpdateResponseSchema schema, - final SszUInt64 responseChunkLen, - final SszBytes4 context, - final LightClientUpdate payload) { - super(schema, responseChunkLen, context, payload); - } - - protected LightClientUpdateResponse( - final LightClientUpdateResponseSchema type, final TreeNode backingNode) { - super(type, backingNode); - } -} diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/lightclient/LightClientUpdateResponseSchema.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/lightclient/LightClientUpdateResponseSchema.java deleted file mode 100644 index 7b725a5c996..00000000000 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/lightclient/LightClientUpdateResponseSchema.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Consensys Software Inc., 2026 - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package tech.pegasys.teku.spec.datastructures.lightclient; - -import static tech.pegasys.teku.spec.schemas.registry.SchemaTypes.LIGHT_CLIENT_UPDATE_SCHEMA; - -import tech.pegasys.teku.infrastructure.ssz.containers.ContainerSchema3; -import tech.pegasys.teku.infrastructure.ssz.primitive.SszBytes4; -import tech.pegasys.teku.infrastructure.ssz.primitive.SszUInt64; -import tech.pegasys.teku.infrastructure.ssz.schema.SszPrimitiveSchemas; -import tech.pegasys.teku.infrastructure.ssz.tree.TreeNode; -import tech.pegasys.teku.spec.schemas.registry.SchemaRegistry; - -public class LightClientUpdateResponseSchema - extends ContainerSchema3 { - - public LightClientUpdateResponseSchema(final SchemaRegistry registry) { - super( - "LightClientUpdateResponse", - namedSchema("response_chunk_len", SszPrimitiveSchemas.UINT64_SCHEMA), - namedSchema("context", SszPrimitiveSchemas.BYTES4_SCHEMA), - namedSchema("payload", registry.get(LIGHT_CLIENT_UPDATE_SCHEMA))); - } - - public LightClientUpdateResponse create( - final SszUInt64 responseChunkLen, final SszBytes4 context, final LightClientUpdate payload) { - return new LightClientUpdateResponse(this, responseChunkLen, context, payload); - } - - @Override - public LightClientUpdateResponse createFromBackingNode(final TreeNode node) { - return new LightClientUpdateResponse(this, node); - } -} diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/metadata/LightClientUpdateWithContext.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/metadata/LightClientUpdateWithContext.java new file mode 100644 index 00000000000..b3a129e688e --- /dev/null +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/metadata/LightClientUpdateWithContext.java @@ -0,0 +1,19 @@ +/* + * Copyright Consensys Software Inc., 2026 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.spec.datastructures.metadata; + +import tech.pegasys.teku.infrastructure.bytes.Bytes4; +import tech.pegasys.teku.spec.datastructures.lightclient.LightClientUpdate; + +public record LightClientUpdateWithContext(Bytes4 context, LightClientUpdate update) {} diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/SchemaDefinitionsAltair.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/SchemaDefinitionsAltair.java index a587854f4af..dd81eba2c9e 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/SchemaDefinitionsAltair.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/SchemaDefinitionsAltair.java @@ -43,7 +43,6 @@ import tech.pegasys.teku.spec.datastructures.lightclient.LightClientFinalityUpdateSchema; import tech.pegasys.teku.spec.datastructures.lightclient.LightClientHeaderSchema; import tech.pegasys.teku.spec.datastructures.lightclient.LightClientOptimisticUpdateSchema; -import tech.pegasys.teku.spec.datastructures.lightclient.LightClientUpdateResponseSchema; import tech.pegasys.teku.spec.datastructures.lightclient.LightClientUpdateSchema; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.metadata.MetadataMessageSchema; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.status.StatusMessageSchema; @@ -83,7 +82,6 @@ public class SchemaDefinitionsAltair extends AbstractSchemaDefinitions { private final LightClientHeaderSchema lightClientHeaderSchema; private final LightClientBootstrapSchema lightClientBootstrapSchema; private final LightClientUpdateSchema lightClientUpdateSchema; - private final LightClientUpdateResponseSchema lightClientUpdateResponseSchema; private final LightClientFinalityUpdateSchema lightClientFinalityUpdateSchema; private final LightClientOptimisticUpdateSchema lightClientOptimisticUpdateSchema; @@ -110,7 +108,6 @@ public SchemaDefinitionsAltair(final SchemaRegistry schemaRegistry) { this.lightClientHeaderSchema = schemaRegistry.get(LIGHT_CLIENT_HEADER_SCHEMA); this.lightClientBootstrapSchema = schemaRegistry.get(LIGHT_CLIENT_BOOTSTRAP_SCHEMA); this.lightClientUpdateSchema = schemaRegistry.get(LIGHT_CLIENT_UPDATE_SCHEMA); - this.lightClientUpdateResponseSchema = new LightClientUpdateResponseSchema(schemaRegistry); this.lightClientFinalityUpdateSchema = schemaRegistry.get(LIGHT_CLIENT_FINALITY_UPDATE_SCHEMA); this.lightClientOptimisticUpdateSchema = schemaRegistry.get(LIGHT_CLIENT_OPTIMISTIC_UPDATE_SCHEMA); @@ -258,10 +255,6 @@ public LightClientUpdateSchema getLightClientUpdateSchema() { return lightClientUpdateSchema; } - public LightClientUpdateResponseSchema getLightClientUpdateResponseSchema() { - return lightClientUpdateResponseSchema; - } - public LightClientFinalityUpdateSchema getLightClientFinalityUpdateSchema() { return lightClientFinalityUpdateSchema; } diff --git a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java index 66bf31cbaa0..948fe2cc791 100644 --- a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java +++ b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java @@ -77,7 +77,6 @@ import tech.pegasys.teku.infrastructure.ssz.collections.SszUInt64Vector; import tech.pegasys.teku.infrastructure.ssz.primitive.SszByte; import tech.pegasys.teku.infrastructure.ssz.primitive.SszBytes32; -import tech.pegasys.teku.infrastructure.ssz.primitive.SszBytes4; import tech.pegasys.teku.infrastructure.ssz.primitive.SszUInt64; import tech.pegasys.teku.infrastructure.ssz.schema.SszListSchema; import tech.pegasys.teku.infrastructure.ssz.schema.SszVectorSchema; @@ -198,12 +197,11 @@ import tech.pegasys.teku.spec.datastructures.lightclient.LightClientOptimisticUpdate; import tech.pegasys.teku.spec.datastructures.lightclient.LightClientOptimisticUpdateSchema; import tech.pegasys.teku.spec.datastructures.lightclient.LightClientUpdate; -import tech.pegasys.teku.spec.datastructures.lightclient.LightClientUpdateResponse; -import tech.pegasys.teku.spec.datastructures.lightclient.LightClientUpdateResponseSchema; import tech.pegasys.teku.spec.datastructures.lightclient.LightClientUpdateSchema; import tech.pegasys.teku.spec.datastructures.lightclient.versions.capella.LightClientHeaderSchemaCapella; import tech.pegasys.teku.spec.datastructures.lightclient.versions.gloas.LightClientHeaderSchemaGloas; import tech.pegasys.teku.spec.datastructures.metadata.BlockContainerAndMetaData; +import tech.pegasys.teku.spec.datastructures.metadata.LightClientUpdateWithContext; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.BlobIdentifier; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.EnrForkId; import tech.pegasys.teku.spec.datastructures.operations.AggregateAndProof; @@ -2546,12 +2544,8 @@ private LightClientHeader lightClientHeaderAtSlot(final UInt64 slot) { .create(randomBeaconBlockHeader(slot, UInt64.ZERO)); } - public LightClientUpdateResponse randomLightClientUpdateResponse(final UInt64 slot) { - final LightClientUpdateResponseSchema schema = - getAltairSchemaDefinitions(slot).getLightClientUpdateResponseSchema(); - - return schema.create( - SszUInt64.of(randomUInt64()), SszBytes4.of(randomBytes4()), randomLightClientUpdate(slot)); + public LightClientUpdateWithContext randomLightClientUpdateWithContext(final UInt64 slot) { + return new LightClientUpdateWithContext(randomBytes4(), randomLightClientUpdate(slot)); } public Withdrawal randomWithdrawal() {