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 c947ad4f481..5e6b43b8677 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 @@ -130,7 +130,7 @@ public static RestApi create( .endpoint(new DeleteFeeRecipient(proposerConfigManager)) .endpoint(new DeleteGasLimit(proposerConfigManager)) .endpoint(new PostVoluntaryExit(voluntaryExitDataProvider)) - .endpoint(new GetGraffiti()) + .endpoint(new GetGraffiti(keyManager)) .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 index 2aec541e9f5..94900c8da13 100644 --- 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 @@ -14,24 +14,29 @@ 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_NOT_FOUND; 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.nio.charset.StandardCharsets; import java.util.Objects; import java.util.Optional; import java.util.function.Function; -import org.apache.commons.lang3.NotImplementedException; +import org.apache.tuweni.bytes.Bytes32; 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; +import tech.pegasys.teku.validator.client.KeyManager; +import tech.pegasys.teku.validator.client.Validator; public class GetGraffiti extends RestApiEndpoint { public static final String ROUTE = "/eth/v1/validator/{pubkey}/graffiti"; + private final KeyManager keyManager; private static final SerializableTypeDefinition GRAFFITI_TYPE = SerializableTypeDefinition.object(GraffitiResponse.class) @@ -45,7 +50,7 @@ public class GetGraffiti extends RestApiEndpoint { .withField("data", GRAFFITI_TYPE, Function.identity()) .build(); - public GetGraffiti() { + public GetGraffiti(final KeyManager keyManager) { super( EndpointMetadata.get(ROUTE) .operationId("getGraffiti") @@ -60,11 +65,25 @@ public GetGraffiti() { .withNotFoundResponse() .withNotImplementedResponse() .build()); + this.keyManager = keyManager; } @Override public void handleRequest(RestApiRequest request) throws JsonProcessingException { - throw new NotImplementedException("Not implemented"); + final BLSPublicKey publicKey = request.getPathParameter(PARAM_PUBKEY_TYPE); + + final Optional maybeValidator = keyManager.getValidatorByPublicKey(publicKey); + if (maybeValidator.isEmpty()) { + request.respondError(SC_NOT_FOUND, "Validator not found"); + return; + } + + String graffiti = maybeValidator.get().getGraffiti().map(this::processGraffitiBytes).orElse(""); + request.respondOk(new GraffitiResponse(publicKey, graffiti)); + } + + private String processGraffitiBytes(final Bytes32 graffiti) { + return new String(graffiti.toArrayUnsafe(), StandardCharsets.UTF_8).strip().replace("\0", ""); } static class GraffitiResponse { 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 index 22cc7f80a79..3e5f4bda765 100644 --- 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 @@ -14,9 +14,14 @@ package tech.pegasys.teku.validator.client.restapi.apis; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_FORBIDDEN; 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_NOT_IMPLEMENTED; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_UNAUTHORIZED; @@ -24,16 +29,87 @@ import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.verifyMetadataErrorResponse; import com.fasterxml.jackson.core.JsonProcessingException; +import java.io.IOException; +import java.util.Optional; +import org.apache.tuweni.bytes.Bytes32; import org.junit.jupiter.api.Test; +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.infrastructure.http.HttpErrorResponse; +import tech.pegasys.teku.infrastructure.restapi.StubRestApiRequest; import tech.pegasys.teku.spec.TestSpecFactory; +import tech.pegasys.teku.spec.signatures.Signer; import tech.pegasys.teku.spec.util.DataStructureUtil; +import tech.pegasys.teku.validator.api.Bytes32Parser; +import tech.pegasys.teku.validator.client.OwnedKeyManager; +import tech.pegasys.teku.validator.client.Validator; class GetGraffitiTest { - private final GetGraffiti handler = new GetGraffiti(); + private final OwnedKeyManager keyManager = mock(OwnedKeyManager.class); + private final GetGraffiti handler = new GetGraffiti(keyManager); + private StubRestApiRequest request; private final DataStructureUtil dataStructureUtil = new DataStructureUtil(TestSpecFactory.createDefault()); + @Test + void shouldGetGraffiti() throws JsonProcessingException { + final BLSPublicKey publicKey = dataStructureUtil.randomPublicKey(); + final String stringGraffiti = "Test graffiti"; + final Bytes32 graffiti = Bytes32Parser.toBytes32(stringGraffiti); + + request = + StubRestApiRequest.builder() + .metadata(handler.getMetadata()) + .pathParameter("pubkey", publicKey.toHexString()) + .build(); + + final Validator validator = + new Validator(publicKey, mock(Signer.class), () -> Optional.of(graffiti)); + when(keyManager.getValidatorByPublicKey(eq(publicKey))).thenReturn(Optional.of(validator)); + + handler.handleRequest(request); + + GetGraffiti.GraffitiResponse expectedResponse = + new GetGraffiti.GraffitiResponse(publicKey, stringGraffiti); + assertThat(request.getResponseCode()).isEqualTo(SC_OK); + assertThat(request.getResponseBody()).isEqualTo(expectedResponse); + } + + @Test + void shouldGetEmptyGraffiti() throws JsonProcessingException { + final BLSPublicKey publicKey = dataStructureUtil.randomPublicKey(); + request = + StubRestApiRequest.builder() + .metadata(handler.getMetadata()) + .pathParameter("pubkey", publicKey.toHexString()) + .build(); + + final Validator validator = new Validator(publicKey, mock(Signer.class), Optional::empty); + when(keyManager.getValidatorByPublicKey(eq(publicKey))).thenReturn(Optional.of(validator)); + + handler.handleRequest(request); + + GetGraffiti.GraffitiResponse expectedResponse = new GetGraffiti.GraffitiResponse(publicKey, ""); + assertThat(request.getResponseCode()).isEqualTo(SC_OK); + assertThat(request.getResponseBody()).isEqualTo(expectedResponse); + } + + @Test + void shouldHandleValidatorNotFound() throws IOException { + request = + StubRestApiRequest.builder() + .metadata(handler.getMetadata()) + .pathParameter("pubkey", dataStructureUtil.randomPublicKey().toHexString()) + .build(); + + when(keyManager.getValidatorByPublicKey(any())).thenReturn(Optional.empty()); + + handler.handleRequest(request); + assertThat(request.getResponseCode()).isEqualTo(SC_NOT_FOUND); + assertThat(request.getResponseBody()) + .isEqualTo(new HttpErrorResponse(SC_NOT_FOUND, "Validator not found")); + } + @Test void metadata_shouldHandle200() throws JsonProcessingException { final GetGraffiti.GraffitiResponse response =