From 064ce08fd39328f4823b09d45ceb6ece90c4a776 Mon Sep 17 00:00:00 2001 From: Courtney <45641759+courtneyeh@users.noreply.github.com> Date: Mon, 15 Apr 2024 12:52:15 +1000 Subject: [PATCH] Add GET graffiti keymanager api (#8199) --- .../client/restapi/ValidatorRestApi.java | 3 + .../client/restapi/apis/GetGraffiti.java | 104 ++++++++++++++++++ .../client/restapi/apis/GetGraffitiTest.java | 73 ++++++++++++ .../_eth_v1_validator_{pubkey}_graffiti.json | 93 ++++++++++++++++ .../restapi/schema/GraffitiResponse.json | 19 ++++ 5 files changed, 292 insertions(+) create mode 100644 validator/client/src/main/java/tech/pegasys/teku/validator/client/restapi/apis/GetGraffiti.java create mode 100644 validator/client/src/test/java/tech/pegasys/teku/validator/client/restapi/apis/GetGraffitiTest.java create mode 100644 validator/client/src/test/resources/tech/pegasys/teku/validator/client/restapi/paths/_eth_v1_validator_{pubkey}_graffiti.json create mode 100644 validator/client/src/test/resources/tech/pegasys/teku/validator/client/restapi/schema/GraffitiResponse.json diff --git a/validator/client/src/main/java/tech/pegasys/teku/validator/client/restapi/ValidatorRestApi.java b/validator/client/src/main/java/tech/pegasys/teku/validator/client/restapi/ValidatorRestApi.java index a910a23906c..c947ad4f481 100644 --- a/validator/client/src/main/java/tech/pegasys/teku/validator/client/restapi/ValidatorRestApi.java +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/restapi/ValidatorRestApi.java @@ -42,6 +42,7 @@ import tech.pegasys.teku.validator.client.restapi.apis.DeleteRemoteKeys; import tech.pegasys.teku.validator.client.restapi.apis.GetFeeRecipient; import tech.pegasys.teku.validator.client.restapi.apis.GetGasLimit; +import tech.pegasys.teku.validator.client.restapi.apis.GetGraffiti; import tech.pegasys.teku.validator.client.restapi.apis.GetKeys; import tech.pegasys.teku.validator.client.restapi.apis.GetRemoteKeys; import tech.pegasys.teku.validator.client.restapi.apis.PostKeys; @@ -57,6 +58,7 @@ public class ValidatorRestApi { public static final String TAG_KEY_MANAGEMENT = "Key Management"; public static final String TAG_FEE_RECIPIENT = "Fee Recipient"; public static final String TAG_GAS_LIMIT = "Gas Limit"; + public static final String TAG_GRAFFITI = "Graffiti"; public static RestApi create( final Spec spec, @@ -128,6 +130,7 @@ public static RestApi create( .endpoint(new DeleteFeeRecipient(proposerConfigManager)) .endpoint(new DeleteGasLimit(proposerConfigManager)) .endpoint(new PostVoluntaryExit(voluntaryExitDataProvider)) + .endpoint(new GetGraffiti()) .sslCertificate(config.getRestApiKeystoreFile(), config.getRestApiKeystorePasswordFile()) .passwordFilePath(validatorApiBearerFile) .build(); diff --git a/validator/client/src/main/java/tech/pegasys/teku/validator/client/restapi/apis/GetGraffiti.java b/validator/client/src/main/java/tech/pegasys/teku/validator/client/restapi/apis/GetGraffiti.java new file mode 100644 index 00000000000..2aec541e9f5 --- /dev/null +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/restapi/apis/GetGraffiti.java @@ -0,0 +1,104 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.validator.client.restapi.apis; + +import static tech.pegasys.teku.ethereum.json.types.SharedApiTypes.PUBKEY_API_TYPE; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.STRING_TYPE; +import static tech.pegasys.teku.validator.client.restapi.ValidatorRestApi.TAG_GRAFFITI; +import static tech.pegasys.teku.validator.client.restapi.ValidatorTypes.PARAM_PUBKEY_TYPE; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import org.apache.commons.lang3.NotImplementedException; +import tech.pegasys.teku.bls.BLSPublicKey; +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; + +public class GetGraffiti extends RestApiEndpoint { + public static final String ROUTE = "/eth/v1/validator/{pubkey}/graffiti"; + + private static final SerializableTypeDefinition GRAFFITI_TYPE = + SerializableTypeDefinition.object(GraffitiResponse.class) + .withOptionalField("pubkey", PUBKEY_API_TYPE, GraffitiResponse::getPublicKey) + .withField("graffiti", STRING_TYPE, GraffitiResponse::getGraffiti) + .build(); + + private static final SerializableTypeDefinition RESPONSE_TYPE = + SerializableTypeDefinition.object(GraffitiResponse.class) + .name("GraffitiResponse") + .withField("data", GRAFFITI_TYPE, Function.identity()) + .build(); + + public GetGraffiti() { + super( + EndpointMetadata.get(ROUTE) + .operationId("getGraffiti") + .summary("Get Graffiti") + .description( + "Get the graffiti for an individual validator. If no graffiti is set explicitly, returns the process-wide default.") + .tags(TAG_GRAFFITI) + .withBearerAuthSecurity() + .pathParam(PARAM_PUBKEY_TYPE) + .response(SC_OK, "Success response", RESPONSE_TYPE) + .withAuthenticationResponses() + .withNotFoundResponse() + .withNotImplementedResponse() + .build()); + } + + @Override + public void handleRequest(RestApiRequest request) throws JsonProcessingException { + throw new NotImplementedException("Not implemented"); + } + + static class GraffitiResponse { + private final Optional publicKey; + private final String graffiti; + + GraffitiResponse(final BLSPublicKey publicKey, final String graffiti) { + this.publicKey = Optional.of(publicKey); + this.graffiti = graffiti; + } + + Optional getPublicKey() { + return publicKey; + } + + String getGraffiti() { + return graffiti; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GraffitiResponse that = (GraffitiResponse) o; + return Objects.equals(publicKey, that.publicKey) && Objects.equals(graffiti, that.graffiti); + } + + @Override + public int hashCode() { + return Objects.hash(publicKey, graffiti); + } + } +} diff --git a/validator/client/src/test/java/tech/pegasys/teku/validator/client/restapi/apis/GetGraffitiTest.java b/validator/client/src/test/java/tech/pegasys/teku/validator/client/restapi/apis/GetGraffitiTest.java new file mode 100644 index 00000000000..22cc7f80a79 --- /dev/null +++ b/validator/client/src/test/java/tech/pegasys/teku/validator/client/restapi/apis/GetGraffitiTest.java @@ -0,0 +1,73 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.validator.client.restapi.apis; + +import static org.assertj.core.api.Assertions.assertThat; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_FORBIDDEN; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_INTERNAL_SERVER_ERROR; +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.http.HttpStatusCodes.SC_UNAUTHORIZED; +import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.getResponseStringFromMetadata; +import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.verifyMetadataErrorResponse; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.spec.TestSpecFactory; +import tech.pegasys.teku.spec.util.DataStructureUtil; + +class GetGraffitiTest { + private final GetGraffiti handler = new GetGraffiti(); + + private final DataStructureUtil dataStructureUtil = + new DataStructureUtil(TestSpecFactory.createDefault()); + + @Test + void metadata_shouldHandle200() throws JsonProcessingException { + final GetGraffiti.GraffitiResponse response = + new GetGraffiti.GraffitiResponse(dataStructureUtil.randomPublicKey(), "Test graffiti"); + final String responseData = getResponseStringFromMetadata(handler, SC_OK, response); + assertThat(responseData) + .isEqualTo( + "{\"data\":{\"pubkey\":" + + "\"0xa4654ac3105a58c7634031b5718c4880c87300f72091cfbc69fe490b71d93a671e00e80a388e1ceb8ea1de112003e976\"," + + "\"graffiti\":\"Test graffiti\"}}"); + } + + @Test + void metadata_shouldHandle400() throws JsonProcessingException { + verifyMetadataErrorResponse(handler, SC_BAD_REQUEST); + } + + @Test + void metadata_shouldHandle401() throws JsonProcessingException { + verifyMetadataErrorResponse(handler, SC_UNAUTHORIZED); + } + + @Test + void metadata_shouldHandle403() throws JsonProcessingException { + verifyMetadataErrorResponse(handler, SC_FORBIDDEN); + } + + @Test + void metadata_shouldHandle500() throws JsonProcessingException { + verifyMetadataErrorResponse(handler, SC_INTERNAL_SERVER_ERROR); + } + + @Test + void metadata_shouldHandle501() throws JsonProcessingException { + verifyMetadataErrorResponse(handler, SC_NOT_IMPLEMENTED); + } +} diff --git a/validator/client/src/test/resources/tech/pegasys/teku/validator/client/restapi/paths/_eth_v1_validator_{pubkey}_graffiti.json b/validator/client/src/test/resources/tech/pegasys/teku/validator/client/restapi/paths/_eth_v1_validator_{pubkey}_graffiti.json new file mode 100644 index 00000000000..e8216a258e8 --- /dev/null +++ b/validator/client/src/test/resources/tech/pegasys/teku/validator/client/restapi/paths/_eth_v1_validator_{pubkey}_graffiti.json @@ -0,0 +1,93 @@ +{ + "get" : { + "tags" : [ "Graffiti" ], + "operationId" : "getGraffiti", + "summary" : "Get Graffiti", + "description" : "Get the graffiti for an individual validator. If no graffiti is set explicitly, returns the process-wide default.", + "parameters" : [ { + "name" : "pubkey", + "required" : true, + "in" : "path", + "schema" : { + "type" : "string", + "pattern" : "^0x[a-fA-F0-9]{96}$", + "example" : "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + } + } ], + "security" : [ { + "bearerAuth" : [ ] + } ], + "responses" : { + "200" : { + "description" : "Success response", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/GraffitiResponse" + } + } + } + }, + "401" : { + "description" : "Unauthorized, no token is found", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HttpErrorResponse" + } + } + } + }, + "403" : { + "description" : "Forbidden, a token is found but is invalid", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HttpErrorResponse" + } + } + } + }, + "404" : { + "description" : "Not found", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HttpErrorResponse" + } + } + } + }, + "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" : { + "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/validator/client/src/test/resources/tech/pegasys/teku/validator/client/restapi/schema/GraffitiResponse.json b/validator/client/src/test/resources/tech/pegasys/teku/validator/client/restapi/schema/GraffitiResponse.json new file mode 100644 index 00000000000..ad17992d071 --- /dev/null +++ b/validator/client/src/test/resources/tech/pegasys/teku/validator/client/restapi/schema/GraffitiResponse.json @@ -0,0 +1,19 @@ +{ + "title" : "GraffitiResponse", + "type" : "object", + "required" : [ "data" ], + "properties" : { + "data" : { + "type" : "object", + "required" : [ "graffiti" ], + "properties" : { + "pubkey" : { + "$ref" : "#/components/schemas/Pubkey" + }, + "graffiti" : { + "type" : "string" + } + } + } + } +} \ No newline at end of file