From d695ed43c1c1f841ff3200112ed277875633db1d Mon Sep 17 00:00:00 2001 From: Courtney <45641759+courtneyeh@users.noreply.github.com> Date: Mon, 20 Nov 2023 15:47:25 +1300 Subject: [PATCH] Add POST state validators endpoint (#7706) --- CHANGELOG.md | 1 + ...1_beacon_states_{state_id}_validators.json | 67 +++++ .../PostStateValidatorsRequestBody.json | 19 ++ .../beaconrestapi/BeaconRestApiTypes.java | 6 +- .../JsonTypeDefinitionBeaconRestApi.java | 2 + .../v1/beacon/GetStateValidators.java | 33 +-- .../v1/beacon/PostStateValidators.java | 135 ++++++++++ .../handlers/v1/beacon/StatusParameter.java | 63 +++++ .../v1/beacon/GetStateValidatorsTest.java | 4 +- .../v1/beacon/PostStateValidatorsTest.java | 239 ++++++++++++++++++ 10 files changed, 533 insertions(+), 36 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/CHANGELOG.md b/CHANGELOG.md index 24496dc394f..b424afccf7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,5 +15,6 @@ the [releases page](https://github.com/Consensys/teku/releases). ### Additions and Improvements - Support to new Beacon APIs `publishBlindedBlockV2` and `publishBlockV2` which introduce broadcast validation parameter. - Added configuration attributes in support of honest validator late block reorg, which adds `REORG_HEAD_WEIGHT_THRESHOLD`, `REORG_PARENT_WEIGHT_THRESHOLD`, and `REORG_MAX_EPOCHS_SINCE_FINALIZATION` to phase 0 configurations. Mainnet values have been added as defaults for configurations that have not explicitly listed them. +- Added POST `/eth/v1/beacon/states/{state_id}/validators` beacon API. ### Bug Fixes 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..580bad1c2d0 --- /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" : [ ], + "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 b44838acf28..718d67e508f 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 @@ -60,7 +60,7 @@ import java.util.Locale; 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; @@ -76,8 +76,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/JsonTypeDefinitionBeaconRestApi.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/JsonTypeDefinitionBeaconRestApi.java index 5f39c9e9ae8..f82289d9859 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; @@ -217,6 +218,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..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 @@ -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; @@ -45,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") @@ -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..68aef3dc6fa --- /dev/null +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidators.java @@ -0,0 +1,135 @@ +/* + * 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.StatusParameter.getApplicableValidatorStatuses; +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.json.types.CoreTypes.STRING_TYPE; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.List; +import java.util.Optional; +import java.util.Set; +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.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 { + private static final DeserializableTypeDefinition REQUEST_TYPE = + DeserializableTypeDefinition.object(RequestBody.class) + .name("PostStateValidatorsRequestBody") + .initializer(RequestBody::new) + .withOptionalField( + "ids", + DeserializableTypeDefinition.listOf(STRING_TYPE), + RequestBody::getMaybeIds, + RequestBody::setIds) + .withOptionalField( + "statuses", + DeserializableTypeDefinition.listOf(STRING_TYPE), + RequestBody::getMaybeStringStatuses, + RequestBody::setStatuses) + .build(); + + private final ChainDataProvider chainDataProvider; + + public PostStateValidators(final DataProvider dataProvider) { + this(dataProvider.getChainDataProvider()); + } + + PostStateValidators(final ChainDataProvider provider) { + super( + EndpointMetadata.post(GetStateValidators.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", GetStateValidators.RESPONSE_TYPE) + .withNotFoundResponse() + .build()); + this.chainDataProvider = provider; + } + + @Override + public void handleRequest(RestApiRequest request) throws JsonProcessingException { + 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); + + 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 Optional> getMaybeIds() { + return ids.isEmpty() ? Optional.empty() : Optional.of(ids); + } + + public void setIds(final Optional> ids) { + ids.ifPresent(i -> this.ids = i); + } + + public List getStatuses() { + return statuses; + } + + public Optional> getMaybeStringStatuses() { + return statuses.isEmpty() + ? Optional.empty() + : Optional.of(statuses.stream().map(Enum::name).toList()); + } + + public void setStatuses(final Optional> statuses) { + statuses.ifPresent(s -> this.statuses = s.stream().map(StatusParameter::parse).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..1f15312f2ac --- /dev/null +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/StatusParameter.java @@ -0,0 +1,63 @@ +/* + * 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.Locale; +import java.util.Set; +import java.util.stream.Collectors; +import tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus; + +public enum StatusParameter { + 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) { + return statusParameters.stream() + .flatMap( + statusParameter -> + Arrays.stream(ValidatorStatus.values()) + .filter( + 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 cf8e40cde04..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 @@ -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; @@ -197,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 new file mode 100644 index 00000000000..1f6967e81e6 --- /dev/null +++ b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostStateValidatorsTest.java @@ -0,0 +1,239 @@ +/* + * 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.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.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.Optional; +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(); + requestBody.setIds(Optional.of(List.of("1", "2", "3", "4"))); + 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(); + requestBody.setStatuses(Optional.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(); + requestBody.setIds(Optional.of(List.of("1"))); + 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 + public void shouldGetValidatorFromStateWithEmptyRequestBody() throws Exception { + final StubRestApiRequest request = + StubRestApiRequest.builder() + .metadata(handler.getMetadata()) + .pathParameter("state_id", "head") + .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); + } + + @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.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))); + } +}