From 32ea567f5d065a8506b90bf5367be119241a40c8 Mon Sep 17 00:00:00 2001 From: courtneyeh Date: Wed, 15 Nov 2023 16:01:00 +1100 Subject: [PATCH 01/13] Add POST state validators endpoint --- ...1_beacon_states_{state_id}_validators.json | 67 ++++++ .../PostStateValidatorsRequestBody.json | 19 ++ .../beaconrestapi/BeaconRestApiTypes.java | 2 +- .../JsonTypeDefinitionBeaconRestApi.java | 2 + .../v1/beacon/GetStateValidators.java | 31 +-- .../v1/beacon/PostStateValidators.java | 148 ++++++++++++ .../handlers/v1/beacon/StatusParameter.java | 48 ++++ .../v1/beacon/GetStateValidatorsTest.java | 1 - .../v1/beacon/PostStateValidatorsTest.java | 225 ++++++++++++++++++ 9 files changed, 511 insertions(+), 32 deletions(-) create mode 100644 data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PostStateValidatorsRequestBody.json create mode 100644 data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java create mode 100644 data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/StatusParameter.java create mode 100644 data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_beacon_states_{state_id}_validators.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_beacon_states_{state_id}_validators.json index 93034c36e44..3a0c74ac22e 100644 --- a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_beacon_states_{state_id}_validators.json +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_beacon_states_{state_id}_validators.json @@ -80,5 +80,72 @@ } } } + }, + "post" : { + "tags" : [ "Beacon" ], + "operationId" : "postStateValidators", + "summary" : "Get validators from state", + "description" : "Returns filterable list of validators with their balance, status and index.", + "parameters" : [ { + "name" : "state_id", + "required" : true, + "in" : "path", + "schema" : { + "type" : "string", + "description" : "State identifier. Can be one of: \"head\" (canonical head in node's view), \"genesis\", \"finalized\", \"justified\", <slot>, <hex encoded stateRoot with 0x prefix>.", + "example" : "head" + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PostStateValidatorsRequestBody" + } + } + } + }, + "responses" : { + "200" : { + "description" : "Request successful", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/GetStateValidatorsResponse" + } + } + } + }, + "404" : { + "description" : "Not found", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HttpErrorResponse" + } + } + } + }, + "400" : { + "description" : "The request could not be processed, check the response for more information.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HttpErrorResponse" + } + } + } + }, + "500" : { + "description" : "Internal server error", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HttpErrorResponse" + } + } + } + } + } } } \ No newline at end of file diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PostStateValidatorsRequestBody.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PostStateValidatorsRequestBody.json new file mode 100644 index 00000000000..e8441d42d6d --- /dev/null +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PostStateValidatorsRequestBody.json @@ -0,0 +1,19 @@ +{ + "title" : "PostStateValidatorsRequestBody", + "type" : "object", + "required" : [ "ids", "statuses" ], + "properties" : { + "ids" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "statuses" : { + "type" : "array", + "items" : { + "type" : "string" + } + } + } +} \ No newline at end of file diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/BeaconRestApiTypes.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/BeaconRestApiTypes.java index 68f76427c72..8f0f8872905 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/BeaconRestApiTypes.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/BeaconRestApiTypes.java @@ -57,7 +57,7 @@ import java.util.function.Function; import org.apache.tuweni.bytes.Bytes32; -import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.GetStateValidators.StatusParameter; +import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter; import tech.pegasys.teku.bls.BLSSignature; import tech.pegasys.teku.infrastructure.http.RestApiConstants; import tech.pegasys.teku.infrastructure.json.types.CoreTypes; diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/JsonTypeDefinitionBeaconRestApi.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/JsonTypeDefinitionBeaconRestApi.java index 9679611566d..2cffccec34c 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/JsonTypeDefinitionBeaconRestApi.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/JsonTypeDefinitionBeaconRestApi.java @@ -66,6 +66,7 @@ import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.PostBlindedBlock; import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.PostBlock; import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.PostProposerSlashing; +import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.PostStateValidators; import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.PostSyncCommittees; import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.PostVoluntaryExit; import tech.pegasys.teku.beaconrestapi.handlers.v1.config.GetDepositContract; @@ -215,6 +216,7 @@ private static RestApi create( .endpoint(new GetStateFork(dataProvider)) .endpoint(new GetStateFinalityCheckpoints(dataProvider)) .endpoint(new GetStateValidators(dataProvider)) + .endpoint(new PostStateValidators(dataProvider)) .endpoint(new GetStateValidator(dataProvider)) .endpoint(new GetStateValidatorBalances(dataProvider)) .endpoint(new GetStateCommittees(dataProvider)) diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/GetStateValidators.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/GetStateValidators.java index a72e5850a64..d4cdf44b39f 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/GetStateValidators.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/GetStateValidators.java @@ -17,6 +17,7 @@ import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.PARAMETER_STATE_ID; import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.STATUS_PARAMETER; import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.GetStateValidator.STATE_VALIDATOR_DATA_TYPE; +import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.getApplicableValidatorStatuses; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.EXECUTION_OPTIMISTIC; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.FINALIZED; @@ -25,11 +26,9 @@ import static tech.pegasys.teku.infrastructure.json.types.SerializableTypeDefinition.listOf; import com.fasterxml.jackson.core.JsonProcessingException; -import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; import tech.pegasys.teku.api.ChainDataProvider; import tech.pegasys.teku.api.DataProvider; import tech.pegasys.teku.api.migrated.StateValidatorData; @@ -96,32 +95,4 @@ public void handleRequest(RestApiRequest request) throws JsonProcessingException .map(AsyncApiResponse::respondOk) .orElseGet(AsyncApiResponse::respondNotFound))); } - - private Set getApplicableValidatorStatuses( - final List statusParameters) { - return statusParameters.stream() - .flatMap( - statusParameter -> - Arrays.stream(ValidatorStatus.values()) - .filter( - validatorStatus -> validatorStatus.name().contains(statusParameter.name()))) - .collect(Collectors.toSet()); - } - - @SuppressWarnings("JavaCase") - public enum StatusParameter { - pending_initialized, - pending_queued, - active_ongoing, - active_exiting, - active_slashed, - exited_unslashed, - exited_slashed, - withdrawal_possible, - withdrawal_done, - active, - pending, - exited, - withdrawal; - } } diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java new file mode 100644 index 00000000000..e92d5c62d52 --- /dev/null +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java @@ -0,0 +1,148 @@ +/* + * Copyright Consensys Software Inc., 2022 + * + * 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.beaconrestapi.handlers.v1.beacon; + +import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.PARAMETER_STATE_ID; +import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.GetStateValidator.STATE_VALIDATOR_DATA_TYPE; +import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.getApplicableValidatorStatuses; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.EXECUTION_OPTIMISTIC; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.FINALIZED; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_BEACON; +import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.BOOLEAN_TYPE; +import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.STRING_TYPE; +import static tech.pegasys.teku.infrastructure.json.types.SerializableTypeDefinition.listOf; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import tech.pegasys.teku.api.ChainDataProvider; +import tech.pegasys.teku.api.DataProvider; +import tech.pegasys.teku.api.migrated.StateValidatorData; +import tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition; +import tech.pegasys.teku.infrastructure.json.types.SerializableTypeDefinition; +import tech.pegasys.teku.infrastructure.restapi.endpoints.AsyncApiResponse; +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.spec.datastructures.metadata.ObjectAndMetaData; + +public class PostStateValidators extends RestApiEndpoint { + public static final String ROUTE = "/eth/v1/beacon/states/{state_id}/validators"; + + private static final SerializableTypeDefinition>> + RESPONSE_TYPE = + SerializableTypeDefinition.>>object() + .name("GetStateValidatorsResponse") + .withField( + EXECUTION_OPTIMISTIC, BOOLEAN_TYPE, ObjectAndMetaData::isExecutionOptimistic) + .withField(FINALIZED, BOOLEAN_TYPE, ObjectAndMetaData::isFinalized) + .withField("data", listOf(STATE_VALIDATOR_DATA_TYPE), ObjectAndMetaData::getData) + .build(); + + private static final DeserializableTypeDefinition REQUEST_TYPE = + DeserializableTypeDefinition.object(RequestBody.class) + .name("PostStateValidatorsRequestBody") + .initializer(RequestBody::new) + .withField( + "ids", + DeserializableTypeDefinition.listOf(STRING_TYPE), + RequestBody::getIds, + RequestBody::setIds) + .withField( + "statuses", + DeserializableTypeDefinition.listOf(STRING_TYPE), + RequestBody::getStringStatuses, + RequestBody::setStatuses) + .build(); + + private final ChainDataProvider chainDataProvider; + + public PostStateValidators(final DataProvider dataProvider) { + this(dataProvider.getChainDataProvider()); + } + + PostStateValidators(final ChainDataProvider provider) { + super( + EndpointMetadata.post(ROUTE) + .operationId("postStateValidators") + .summary("Get validators from state") + .description( + "Returns filterable list of validators with their balance, status and index.") + .pathParam(PARAMETER_STATE_ID) + .requestBodyType(REQUEST_TYPE) + .tags(TAG_BEACON) + .response(SC_OK, "Request successful", RESPONSE_TYPE) + .withNotFoundResponse() + .build()); + this.chainDataProvider = provider; + } + + @Override + public void handleRequest(RestApiRequest request) throws JsonProcessingException { + final RequestBody requestBody = request.getRequestBody(); + + final List validators = requestBody.getIds(); + final List statusParameters = requestBody.getStatuses(); + + final Set statusFilter = getApplicableValidatorStatuses(statusParameters); + + SafeFuture>>> future = + chainDataProvider.getStateValidators( + request.getPathParameter(PARAMETER_STATE_ID), validators, statusFilter); + + request.respondAsync( + future.thenApply( + maybeData -> + maybeData + .map(AsyncApiResponse::respondOk) + .orElseGet(AsyncApiResponse::respondNotFound))); + } + + static class RequestBody { + private List ids = List.of(); + private List statuses = List.of(); + + RequestBody() {} + + public RequestBody(final List ids, final List statuses) { + this.ids = ids; + this.statuses = statuses; + } + + public List getIds() { + return ids; + } + + public void setIds(final List ids) { + this.ids = ids; + } + + public List getStatuses() { + return statuses; + } + + public List getStringStatuses() { + return statuses.stream().map(Enum::name).collect(Collectors.toList()); + } + + public void setStatuses(final List statuses) { + this.statuses = statuses.stream().map(StatusParameter::valueOf).toList(); + } + } +} diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/StatusParameter.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/StatusParameter.java new file mode 100644 index 00000000000..48fc2490943 --- /dev/null +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/StatusParameter.java @@ -0,0 +1,48 @@ +/* + * Copyright Consensys Software Inc., 2023 + * + * 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.beaconrestapi.handlers.v1.beacon; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus; + +@SuppressWarnings("JavaCase") +public enum StatusParameter { + pending_initialized, + pending_queued, + active_ongoing, + active_exiting, + active_slashed, + exited_unslashed, + exited_slashed, + withdrawal_possible, + withdrawal_done, + active, + pending, + exited, + withdrawal; + + public static Set getApplicableValidatorStatuses( + final List statusParameters) { + return statusParameters.stream() + .flatMap( + statusParameter -> + Arrays.stream(ValidatorStatus.values()) + .filter( + validatorStatus -> validatorStatus.name().contains(statusParameter.name()))) + .collect(Collectors.toSet()); + } +} diff --git a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/GetStateValidatorsTest.java b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/GetStateValidatorsTest.java index cf8e40cde04..c3f7ec8244b 100644 --- a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/GetStateValidatorsTest.java +++ b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/GetStateValidatorsTest.java @@ -51,7 +51,6 @@ import tech.pegasys.teku.api.migrated.StateValidatorData; import tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus; import tech.pegasys.teku.beaconrestapi.AbstractMigratedBeaconHandlerWithChainDataProviderTest; -import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.GetStateValidators.StatusParameter; import tech.pegasys.teku.infrastructure.restapi.StubRestApiRequest; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.SpecMilestone; diff --git a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java new file mode 100644 index 00000000000..6e7b6e0ff32 --- /dev/null +++ b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java @@ -0,0 +1,225 @@ +/* + * Copyright Consensys Software Inc., 2023 + * + * 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.beaconrestapi.handlers.v1.beacon; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.emptySet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.active_exiting; +import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.active_ongoing; +import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.active_slashed; +import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.exited_slashed; +import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.pending_initialized; +import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.pending_queued; +import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.withdrawal_done; +import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.withdrawal_possible; +import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.active; +import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.exited; +import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.exited_unslashed; +import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.pending; +import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.withdrawal; +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_FOUND; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.getResponseStringFromMetadata; +import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.verifyMetadataEmptyResponse; +import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.verifyMetadataErrorResponse; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.io.Resources; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import tech.pegasys.teku.api.exceptions.BadRequestException; +import tech.pegasys.teku.api.migrated.StateValidatorData; +import tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus; +import tech.pegasys.teku.beaconrestapi.AbstractMigratedBeaconHandlerWithChainDataProviderTest; +import tech.pegasys.teku.infrastructure.restapi.StubRestApiRequest; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.datastructures.metadata.ObjectAndMetaData; + +class PostStateValidatorsTest extends AbstractMigratedBeaconHandlerWithChainDataProviderTest { + + @BeforeEach + void setup() { + initialise(SpecMilestone.ALTAIR); + genesis(); + + setHandler(new PostStateValidators(chainDataProvider)); + } + + @Test + public void shouldGetValidatorFromState() throws Exception { + final PostStateValidators.RequestBody requestBody = + new PostStateValidators.RequestBody(List.of("1", "2", "3", "4"), List.of()); + final StubRestApiRequest request = + StubRestApiRequest.builder() + .metadata(handler.getMetadata()) + .pathParameter("state_id", "head") + .build(); + request.setRequestBody(requestBody); + + final ObjectAndMetaData> expectedResponse = + chainDataProvider + .getStateValidators("head", List.of("1", "2", "3", "4"), emptySet()) + .get() + .orElseThrow(); + + handler.handleRequest(request); + + assertThat(request.getResponseCode()).isEqualTo(SC_OK); + assertThat(request.getResponseBody()).isEqualTo(expectedResponse); + } + + @Test + public void shouldGetValidatorFromStateWithList() throws Exception { + final PostStateValidators.RequestBody requestBody = + new PostStateValidators.RequestBody( + List.of("1", "2"), + List.of( + StatusParameter.active_ongoing, + StatusParameter.active_exiting, + StatusParameter.withdrawal_done)); + final StubRestApiRequest request = + StubRestApiRequest.builder() + .metadata(handler.getMetadata()) + .pathParameter("state_id", "head") + .build(); + request.setRequestBody(requestBody); + + final ObjectAndMetaData> expectedResponse = + chainDataProvider + .getStateValidators( + "head", List.of("1", "2"), Set.of(active_ongoing, active_exiting, withdrawal_done)) + .get() + .orElseThrow(); + + handler.handleRequest(request); + + assertThat(request.getResponseCode()).isEqualTo(SC_OK); + assertThat(request.getResponseBody()).isEqualTo(expectedResponse); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("provideStatusParameters") + public void shouldGetValidatorsByStatusParameter( + final List statusParameters, + final Set expectedValidatorStatuses) + throws Exception { + final PostStateValidators.RequestBody requestBody = + new PostStateValidators.RequestBody(List.of(), statusParameters); + final StubRestApiRequest request = + StubRestApiRequest.builder() + .metadata(handler.getMetadata()) + .pathParameter("state_id", "head") + .build(); + request.setRequestBody(requestBody); + + final ObjectAndMetaData> expectedResponse = + chainDataProvider + .getStateValidators("head", List.of(), expectedValidatorStatuses) + .get() + .orElseThrow(); + + handler.handleRequest(request); + + assertThat(request.getResponseCode()).isEqualTo(SC_OK); + assertThat(request.getResponseBody()).isEqualTo(expectedResponse); + } + + @Test + public void shouldGetBadRequestForInvalidState() { + final PostStateValidators.RequestBody requestBody = + new PostStateValidators.RequestBody(List.of("1"), List.of()); + final StubRestApiRequest request = + StubRestApiRequest.builder() + .metadata(handler.getMetadata()) + .pathParameter("state_id", "invalid") + .build(); + request.setRequestBody(requestBody); + + assertThatThrownBy(() -> handler.handleRequest(request)) + .isInstanceOf(BadRequestException.class) + .hasMessageContaining("Invalid state ID: invalid"); + } + + @Test + void metadata_shouldHandle400() throws JsonProcessingException { + verifyMetadataErrorResponse(handler, SC_BAD_REQUEST); + } + + @Test + void metadata_shouldHandle404() { + verifyMetadataEmptyResponse(new GetGenesis(chainDataProvider), SC_NOT_FOUND); + } + + @Test + void metadata_shouldHandle500() throws JsonProcessingException { + verifyMetadataErrorResponse(handler, SC_INTERNAL_SERVER_ERROR); + } + + @Test + void metadata_shouldHandle200() throws IOException { + final StateValidatorData data1 = + new StateValidatorData( + UInt64.valueOf(0), + dataStructureUtil.randomUInt64(), + active_ongoing, + dataStructureUtil.randomValidator()); + final StateValidatorData data2 = + new StateValidatorData( + UInt64.valueOf(1), + dataStructureUtil.randomUInt64(), + active_ongoing, + dataStructureUtil.randomValidator()); + final List value = List.of(data1, data2); + final ObjectAndMetaData> responseData = withMetaData(value); + + final String data = getResponseStringFromMetadata(handler, SC_OK, responseData); + final String expected = + Resources.toString( + Resources.getResource(GetStateValidatorsTest.class, "getStateValidatorsTest.json"), + UTF_8); + AssertionsForClassTypes.assertThat(data).isEqualTo(expected); + } + + @Test + void statusParameterEnumContainsAllValidatorStatuses() { + assertThat(ValidatorStatus.values()) + .allMatch( + validatorStatus -> + Arrays.stream(StatusParameter.values()) + .anyMatch( + statusParameter -> statusParameter.name().equals(validatorStatus.name()))); + } + + private static Stream provideStatusParameters() { + return Stream.of( + Arguments.of(List.of(active), Set.of(active_ongoing, active_exiting, active_slashed)), + Arguments.of(List.of(pending), Set.of(pending_initialized, pending_queued)), + Arguments.of(List.of(exited), Set.of(exited_slashed, exited_unslashed)), + Arguments.of(List.of(withdrawal), Set.of(withdrawal_done, withdrawal_possible))); + } +} From 7a5d2da3ead807f9e54edf1831854728180b0da6 Mon Sep 17 00:00:00 2001 From: courtneyeh Date: Wed, 15 Nov 2023 16:06:19 +1100 Subject: [PATCH 02/13] Added CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5c1973360d..b5d384f0a90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,5 +13,6 @@ the [releases page](https://github.com/Consensys/teku/releases). - By default, Teku won't allow syncing from genesis, users should use `--checkpoint-sync-url` when starting a new node. It is possible to revert back to the previous behaviour using the flag `--ignore-weak-subjectivity-period-enabled`. ### Additions and Improvements +- Added POST `/eth/v1/beacon/states/{state_id}/validators` beacon API. ### Bug Fixes From c37db088507728ff74480220c9779c356fe7d90a Mon Sep 17 00:00:00 2001 From: courtneyeh Date: Thu, 16 Nov 2023 09:27:32 +1100 Subject: [PATCH 03/13] Reuse response type --- .../handlers/v1/beacon/GetStateValidators.java | 2 +- .../v1/beacon/PostStateValidators.java | 18 +----------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/GetStateValidators.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/GetStateValidators.java index d4cdf44b39f..046a6a7e3de 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/GetStateValidators.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/GetStateValidators.java @@ -44,7 +44,7 @@ public class GetStateValidators extends RestApiEndpoint { public static final String ROUTE = "/eth/v1/beacon/states/{state_id}/validators"; - private static final SerializableTypeDefinition>> + static final SerializableTypeDefinition>> RESPONSE_TYPE = SerializableTypeDefinition.>>object() .name("GetStateValidatorsResponse") diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java index e92d5c62d52..bd01fc90cff 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java @@ -14,15 +14,10 @@ package tech.pegasys.teku.beaconrestapi.handlers.v1.beacon; import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.PARAMETER_STATE_ID; -import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.GetStateValidator.STATE_VALIDATOR_DATA_TYPE; import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.getApplicableValidatorStatuses; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; -import static tech.pegasys.teku.infrastructure.http.RestApiConstants.EXECUTION_OPTIMISTIC; -import static tech.pegasys.teku.infrastructure.http.RestApiConstants.FINALIZED; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_BEACON; -import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.BOOLEAN_TYPE; import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.STRING_TYPE; -import static tech.pegasys.teku.infrastructure.json.types.SerializableTypeDefinition.listOf; import com.fasterxml.jackson.core.JsonProcessingException; import java.util.List; @@ -35,7 +30,6 @@ import tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition; -import tech.pegasys.teku.infrastructure.json.types.SerializableTypeDefinition; import tech.pegasys.teku.infrastructure.restapi.endpoints.AsyncApiResponse; import tech.pegasys.teku.infrastructure.restapi.endpoints.EndpointMetadata; import tech.pegasys.teku.infrastructure.restapi.endpoints.RestApiEndpoint; @@ -45,16 +39,6 @@ public class PostStateValidators extends RestApiEndpoint { public static final String ROUTE = "/eth/v1/beacon/states/{state_id}/validators"; - private static final SerializableTypeDefinition>> - RESPONSE_TYPE = - SerializableTypeDefinition.>>object() - .name("GetStateValidatorsResponse") - .withField( - EXECUTION_OPTIMISTIC, BOOLEAN_TYPE, ObjectAndMetaData::isExecutionOptimistic) - .withField(FINALIZED, BOOLEAN_TYPE, ObjectAndMetaData::isFinalized) - .withField("data", listOf(STATE_VALIDATOR_DATA_TYPE), ObjectAndMetaData::getData) - .build(); - private static final DeserializableTypeDefinition REQUEST_TYPE = DeserializableTypeDefinition.object(RequestBody.class) .name("PostStateValidatorsRequestBody") @@ -87,7 +71,7 @@ public PostStateValidators(final DataProvider dataProvider) { .pathParam(PARAMETER_STATE_ID) .requestBodyType(REQUEST_TYPE) .tags(TAG_BEACON) - .response(SC_OK, "Request successful", RESPONSE_TYPE) + .response(SC_OK, "Request successful", GetStateValidators.RESPONSE_TYPE) .withNotFoundResponse() .build()); this.chainDataProvider = provider; From 048e09ab81e418aef13122822ca60c7b83e91a84 Mon Sep 17 00:00:00 2001 From: courtneyeh Date: Thu, 16 Nov 2023 14:33:18 +1100 Subject: [PATCH 04/13] Make request lists optional fields --- .../v1/beacon/PostStateValidators.java | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java index bd01fc90cff..09659e44bf0 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java @@ -43,12 +43,12 @@ public class PostStateValidators extends RestApiEndpoint { DeserializableTypeDefinition.object(RequestBody.class) .name("PostStateValidatorsRequestBody") .initializer(RequestBody::new) - .withField( + .withOptionalField( "ids", DeserializableTypeDefinition.listOf(STRING_TYPE), RequestBody::getIds, RequestBody::setIds) - .withField( + .withOptionalField( "statuses", DeserializableTypeDefinition.listOf(STRING_TYPE), RequestBody::getStringStatuses, @@ -81,7 +81,7 @@ public PostStateValidators(final DataProvider dataProvider) { public void handleRequest(RestApiRequest request) throws JsonProcessingException { final RequestBody requestBody = request.getRequestBody(); - final List validators = requestBody.getIds(); + final List validators = requestBody.getIds().orElse(List.of()); final List statusParameters = requestBody.getStatuses(); final Set statusFilter = getApplicableValidatorStatuses(statusParameters); @@ -109,24 +109,26 @@ public RequestBody(final List ids, final List statuses) this.statuses = statuses; } - public List getIds() { - return ids; + public Optional> getIds() { + return ids.isEmpty() ? Optional.empty() : Optional.of(ids); } - public void setIds(final List ids) { - this.ids = ids; + public void setIds(final Optional> ids) { + ids.ifPresent(i -> this.ids = i); } public List getStatuses() { return statuses; } - public List getStringStatuses() { - return statuses.stream().map(Enum::name).collect(Collectors.toList()); + public Optional> getStringStatuses() { + return statuses.isEmpty() + ? Optional.empty() + : Optional.of(statuses.stream().map(Enum::name).collect(Collectors.toList())); } - public void setStatuses(final List statuses) { - this.statuses = statuses.stream().map(StatusParameter::valueOf).toList(); + public void setStatuses(final Optional> statuses) { + statuses.ifPresent(s -> this.statuses = s.stream().map(StatusParameter::valueOf).toList()); } } } From 9230bfe8e8ad029360b459635850646abedd4824 Mon Sep 17 00:00:00 2001 From: courtneyeh Date: Thu, 16 Nov 2023 15:07:55 +1100 Subject: [PATCH 05/13] Update test --- .../handlers/v1/beacon/PostStateValidatorsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java index 6e7b6e0ff32..ca927c71618 100644 --- a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java +++ b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java @@ -21,13 +21,13 @@ import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.active_ongoing; import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.active_slashed; import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.exited_slashed; +import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.exited_unslashed; import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.pending_initialized; import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.pending_queued; import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.withdrawal_done; import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.withdrawal_possible; import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.active; import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.exited; -import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.exited_unslashed; import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.pending; import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.withdrawal; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; From 7dd632b039542c5c570878410c854f68684e4978 Mon Sep 17 00:00:00 2001 From: courtneyeh Date: Thu, 16 Nov 2023 15:24:45 +1100 Subject: [PATCH 06/13] Fix enum case warning --- .../beaconrestapi/BeaconRestApiTypes.java | 4 +- .../handlers/v1/beacon/StatusParameter.java | 45 ++++++++++++------- .../v1/beacon/GetStateValidatorsTest.java | 3 +- .../v1/beacon/PostStateValidatorsTest.java | 25 ++++++----- 4 files changed, 47 insertions(+), 30 deletions(-) diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/BeaconRestApiTypes.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/BeaconRestApiTypes.java index 8f0f8872905..5d079e9f51b 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/BeaconRestApiTypes.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/BeaconRestApiTypes.java @@ -72,8 +72,8 @@ public class BeaconRestApiTypes { private static final StringValueTypeDefinition STATUS_VALUE = DeserializableTypeDefinition.string(StatusParameter.class) - .formatter(StatusParameter::toString) - .parser(StatusParameter::valueOf) + .formatter(StatusParameter::getValue) + .parser(StatusParameter::parse) .example("active_ongoing") .description("ValidatorStatus string") .format("string") diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/StatusParameter.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/StatusParameter.java index 48fc2490943..1f15312f2ac 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/StatusParameter.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/StatusParameter.java @@ -15,25 +15,39 @@ import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.stream.Collectors; import tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus; -@SuppressWarnings("JavaCase") public enum StatusParameter { - pending_initialized, - pending_queued, - active_ongoing, - active_exiting, - active_slashed, - exited_unslashed, - exited_slashed, - withdrawal_possible, - withdrawal_done, - active, - pending, - exited, - withdrawal; + PENDING_INITIALIZED("pending_initialized"), + PENDING_QUEUED("pending_queued"), + ACTIVE_ONGOING("active_ongoing"), + ACTIVE_EXITING("active_exiting"), + ACTIVE_SLASHED("active_slashed"), + EXITED_UNSLASHED("exited_unslashed"), + EXITED_SLASHED("exited_slashed"), + WITHDRAWAL_POSSIBLE("withdrawal_possible"), + WITHDRAWAL_DONE("withdrawal_done"), + ACTIVE("active"), + PENDING("pending"), + EXITED("exited"), + WITHDRAWAL("withdrawal"); + + private final String value; + + StatusParameter(final String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static StatusParameter parse(final String value) { + return StatusParameter.valueOf(value.toUpperCase(Locale.ROOT)); + } public static Set getApplicableValidatorStatuses( final List statusParameters) { @@ -42,7 +56,8 @@ public static Set getApplicableValidatorStatuses( statusParameter -> Arrays.stream(ValidatorStatus.values()) .filter( - validatorStatus -> validatorStatus.name().contains(statusParameter.name()))) + validatorStatus -> + validatorStatus.name().contains(statusParameter.getValue()))) .collect(Collectors.toSet()); } } diff --git a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/GetStateValidatorsTest.java b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/GetStateValidatorsTest.java index c3f7ec8244b..17a47e24a23 100644 --- a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/GetStateValidatorsTest.java +++ b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/GetStateValidatorsTest.java @@ -196,7 +196,8 @@ void statusParameterEnumContainsAllValidatorStatuses() { validatorStatus -> Arrays.stream(StatusParameter.values()) .anyMatch( - statusParameter -> statusParameter.name().equals(validatorStatus.name()))); + statusParameter -> + statusParameter.getValue().equals(validatorStatus.name()))); } private static Stream provideStatusParameters() { diff --git a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java index ca927c71618..4f9f12cf1e2 100644 --- a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java +++ b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java @@ -26,10 +26,10 @@ import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.pending_queued; import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.withdrawal_done; import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.withdrawal_possible; -import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.active; -import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.exited; -import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.pending; -import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.withdrawal; +import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.ACTIVE; +import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.EXITED; +import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.PENDING; +import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.WITHDRAWAL; 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_FOUND; @@ -99,9 +99,9 @@ public void shouldGetValidatorFromStateWithList() throws Exception { new PostStateValidators.RequestBody( List.of("1", "2"), List.of( - StatusParameter.active_ongoing, - StatusParameter.active_exiting, - StatusParameter.withdrawal_done)); + StatusParameter.ACTIVE_ONGOING, + StatusParameter.ACTIVE_EXITING, + StatusParameter.WITHDRAWAL_DONE)); final StubRestApiRequest request = StubRestApiRequest.builder() .metadata(handler.getMetadata()) @@ -212,14 +212,15 @@ void statusParameterEnumContainsAllValidatorStatuses() { validatorStatus -> Arrays.stream(StatusParameter.values()) .anyMatch( - statusParameter -> statusParameter.name().equals(validatorStatus.name()))); + statusParameter -> + statusParameter.getValue().equals(validatorStatus.name()))); } private static Stream provideStatusParameters() { return Stream.of( - Arguments.of(List.of(active), Set.of(active_ongoing, active_exiting, active_slashed)), - Arguments.of(List.of(pending), Set.of(pending_initialized, pending_queued)), - Arguments.of(List.of(exited), Set.of(exited_slashed, exited_unslashed)), - Arguments.of(List.of(withdrawal), Set.of(withdrawal_done, withdrawal_possible))); + Arguments.of(List.of(ACTIVE), Set.of(active_ongoing, active_exiting, active_slashed)), + Arguments.of(List.of(PENDING), Set.of(pending_initialized, pending_queued)), + Arguments.of(List.of(EXITED), Set.of(exited_slashed, exited_unslashed)), + Arguments.of(List.of(WITHDRAWAL), Set.of(withdrawal_done, withdrawal_possible))); } } From d4ee6a84e14ead96ab1f29d6bdb2bfcee7044bf1 Mon Sep 17 00:00:00 2001 From: courtneyeh Date: Thu, 16 Nov 2023 15:26:40 +1100 Subject: [PATCH 07/13] Fix integration tests --- .../beacon/schema/PostStateValidatorsRequestBody.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PostStateValidatorsRequestBody.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PostStateValidatorsRequestBody.json index e8441d42d6d..580bad1c2d0 100644 --- a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PostStateValidatorsRequestBody.json +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PostStateValidatorsRequestBody.json @@ -1,7 +1,7 @@ { "title" : "PostStateValidatorsRequestBody", "type" : "object", - "required" : [ "ids", "statuses" ], + "required" : [ ], "properties" : { "ids" : { "type" : "array", From 4a95554fb96dbc38bc4929fcf31ae5f423edd9a3 Mon Sep 17 00:00:00 2001 From: courtneyeh Date: Fri, 17 Nov 2023 09:54:01 +1100 Subject: [PATCH 08/13] Update testing and fix bug --- .../v1/beacon/PostStateValidators.java | 2 +- .../v1/beacon/PostStateValidatorsTest.java | 28 ++++++++----------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java index 09659e44bf0..398d47d6109 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java @@ -128,7 +128,7 @@ public Optional> getStringStatuses() { } public void setStatuses(final Optional> statuses) { - statuses.ifPresent(s -> this.statuses = s.stream().map(StatusParameter::valueOf).toList()); + statuses.ifPresent(s -> this.statuses = s.stream().map(StatusParameter::parse).toList()); } } } diff --git a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java index 4f9f12cf1e2..563ded79377 100644 --- a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java +++ b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java @@ -26,10 +26,6 @@ import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.pending_queued; import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.withdrawal_done; import static tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus.withdrawal_possible; -import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.ACTIVE; -import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.EXITED; -import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.PENDING; -import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.StatusParameter.WITHDRAWAL; 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_FOUND; @@ -43,6 +39,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import org.assertj.core.api.AssertionsForClassTypes; @@ -72,8 +69,8 @@ void setup() { @Test public void shouldGetValidatorFromState() throws Exception { - final PostStateValidators.RequestBody requestBody = - new PostStateValidators.RequestBody(List.of("1", "2", "3", "4"), List.of()); + final PostStateValidators.RequestBody requestBody = new PostStateValidators.RequestBody(); + requestBody.setIds(Optional.of(List.of("1", "2", "3", "4"))); final StubRestApiRequest request = StubRestApiRequest.builder() .metadata(handler.getMetadata()) @@ -125,11 +122,10 @@ public void shouldGetValidatorFromStateWithList() throws Exception { @ParameterizedTest(name = "{0}") @MethodSource("provideStatusParameters") public void shouldGetValidatorsByStatusParameter( - final List statusParameters, - final Set expectedValidatorStatuses) + final List statusParameters, final Set expectedValidatorStatuses) throws Exception { - final PostStateValidators.RequestBody requestBody = - new PostStateValidators.RequestBody(List.of(), statusParameters); + final PostStateValidators.RequestBody requestBody = new PostStateValidators.RequestBody(); + requestBody.setStatuses(Optional.of(statusParameters)); final StubRestApiRequest request = StubRestApiRequest.builder() .metadata(handler.getMetadata()) @@ -151,8 +147,8 @@ public void shouldGetValidatorsByStatusParameter( @Test public void shouldGetBadRequestForInvalidState() { - final PostStateValidators.RequestBody requestBody = - new PostStateValidators.RequestBody(List.of("1"), List.of()); + final PostStateValidators.RequestBody requestBody = new PostStateValidators.RequestBody(); + requestBody.setIds(Optional.of(List.of("1"))); final StubRestApiRequest request = StubRestApiRequest.builder() .metadata(handler.getMetadata()) @@ -218,9 +214,9 @@ void statusParameterEnumContainsAllValidatorStatuses() { private static Stream provideStatusParameters() { return Stream.of( - Arguments.of(List.of(ACTIVE), Set.of(active_ongoing, active_exiting, active_slashed)), - Arguments.of(List.of(PENDING), Set.of(pending_initialized, pending_queued)), - Arguments.of(List.of(EXITED), Set.of(exited_slashed, exited_unslashed)), - Arguments.of(List.of(WITHDRAWAL), Set.of(withdrawal_done, withdrawal_possible))); + Arguments.of(List.of("active"), Set.of(active_ongoing, active_exiting, active_slashed)), + Arguments.of(List.of("pending"), Set.of(pending_initialized, pending_queued)), + Arguments.of(List.of("exited"), Set.of(exited_slashed, exited_unslashed)), + Arguments.of(List.of("withdrawal"), Set.of(withdrawal_done, withdrawal_possible))); } } From bdf81c5f3e699b258d2e88731f75e4298cb2fb52 Mon Sep 17 00:00:00 2001 From: courtneyeh Date: Fri, 17 Nov 2023 10:34:33 +1100 Subject: [PATCH 09/13] Use optional request body --- .../v1/beacon/PostStateValidators.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java index 398d47d6109..e15a547fa54 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java @@ -46,12 +46,12 @@ public class PostStateValidators extends RestApiEndpoint { .withOptionalField( "ids", DeserializableTypeDefinition.listOf(STRING_TYPE), - RequestBody::getIds, + RequestBody::getMaybeIds, RequestBody::setIds) .withOptionalField( "statuses", DeserializableTypeDefinition.listOf(STRING_TYPE), - RequestBody::getStringStatuses, + RequestBody::getMaybeStringStatuses, RequestBody::setStatuses) .build(); @@ -79,10 +79,10 @@ public PostStateValidators(final DataProvider dataProvider) { @Override public void handleRequest(RestApiRequest request) throws JsonProcessingException { - final RequestBody requestBody = request.getRequestBody(); - - final List validators = requestBody.getIds().orElse(List.of()); - final List statusParameters = requestBody.getStatuses(); + final Optional requestBody = request.getOptionalRequestBody(); + final List validators = requestBody.map(RequestBody::getIds).orElse(List.of()); + final List statusParameters = + requestBody.map(RequestBody::getStatuses).orElse(List.of()); final Set statusFilter = getApplicableValidatorStatuses(statusParameters); @@ -109,7 +109,11 @@ public RequestBody(final List ids, final List statuses) this.statuses = statuses; } - public Optional> getIds() { + public List getIds() { + return ids; + } + + public Optional> getMaybeIds() { return ids.isEmpty() ? Optional.empty() : Optional.of(ids); } @@ -121,7 +125,7 @@ public List getStatuses() { return statuses; } - public Optional> getStringStatuses() { + public Optional> getMaybeStringStatuses() { return statuses.isEmpty() ? Optional.empty() : Optional.of(statuses.stream().map(Enum::name).collect(Collectors.toList())); From 1da2dc3b059a388f2653cb4f4337d9e663803360 Mon Sep 17 00:00:00 2001 From: courtneyeh Date: Fri, 17 Nov 2023 11:10:34 +1100 Subject: [PATCH 10/13] Reuse GetStateValidators route --- .../beaconrestapi/handlers/v1/beacon/PostStateValidators.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java index e15a547fa54..abd3bf1fe0f 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java @@ -37,8 +37,6 @@ import tech.pegasys.teku.spec.datastructures.metadata.ObjectAndMetaData; public class PostStateValidators extends RestApiEndpoint { - public static final String ROUTE = "/eth/v1/beacon/states/{state_id}/validators"; - private static final DeserializableTypeDefinition REQUEST_TYPE = DeserializableTypeDefinition.object(RequestBody.class) .name("PostStateValidatorsRequestBody") @@ -63,7 +61,7 @@ public PostStateValidators(final DataProvider dataProvider) { PostStateValidators(final ChainDataProvider provider) { super( - EndpointMetadata.post(ROUTE) + EndpointMetadata.post(GetStateValidators.ROUTE) .operationId("postStateValidators") .summary("Get validators from state") .description( From 7bea017d698a74126f36c769ff1c333fa7413f2e Mon Sep 17 00:00:00 2001 From: courtneyeh Date: Fri, 17 Nov 2023 11:54:27 +1100 Subject: [PATCH 11/13] Add test for empty request body --- .../v1/beacon/PostStateValidatorsTest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java index 563ded79377..745876394ac 100644 --- a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java +++ b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java @@ -161,6 +161,23 @@ public void shouldGetBadRequestForInvalidState() { .hasMessageContaining("Invalid state ID: invalid"); } + @Test + public void shouldGetValidatorFromStateWithEmptyRequestBody() throws Exception { + final StubRestApiRequest request = + StubRestApiRequest.builder() + .metadata(handler.getMetadata()) + .pathParameter("state_id", "invalid") + .build(); + + final ObjectAndMetaData> expectedResponse = + chainDataProvider.getStateValidators("head", List.of(), Set.of()).get().orElseThrow(); + + handler.handleRequest(request); + + assertThat(request.getResponseCode()).isEqualTo(SC_OK); + assertThat(request.getResponseBody()).isEqualTo(expectedResponse); + } + @Test void metadata_shouldHandle400() throws JsonProcessingException { verifyMetadataErrorResponse(handler, SC_BAD_REQUEST); From 0ebffedc586837f7db9709843e53ed08f8c276c5 Mon Sep 17 00:00:00 2001 From: courtneyeh Date: Fri, 17 Nov 2023 13:06:29 +1100 Subject: [PATCH 12/13] Fix unit test bug --- .../handlers/v1/beacon/PostStateValidatorsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java index 745876394ac..1f6967e81e6 100644 --- a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java +++ b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java @@ -166,7 +166,7 @@ public void shouldGetValidatorFromStateWithEmptyRequestBody() throws Exception { final StubRestApiRequest request = StubRestApiRequest.builder() .metadata(handler.getMetadata()) - .pathParameter("state_id", "invalid") + .pathParameter("state_id", "head") .build(); final ObjectAndMetaData> expectedResponse = From 9e1549f622bd8047347115435dace95bb6806c4a Mon Sep 17 00:00:00 2001 From: courtneyeh Date: Mon, 20 Nov 2023 13:26:47 +1100 Subject: [PATCH 13/13] Small fix --- .../beaconrestapi/handlers/v1/beacon/PostStateValidators.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java index abd3bf1fe0f..68aef3dc6fa 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java @@ -23,7 +23,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; import tech.pegasys.teku.api.ChainDataProvider; import tech.pegasys.teku.api.DataProvider; import tech.pegasys.teku.api.migrated.StateValidatorData; @@ -126,7 +125,7 @@ public List getStatuses() { public Optional> getMaybeStringStatuses() { return statuses.isEmpty() ? Optional.empty() - : Optional.of(statuses.stream().map(Enum::name).collect(Collectors.toList())); + : Optional.of(statuses.stream().map(Enum::name).toList()); } public void setStatuses(final Optional> statuses) {