Skip to content

Commit 22d4edf

Browse files
committed
GetGraffiti
1 parent c8a800c commit 22d4edf

File tree

5 files changed

+369
-0
lines changed

5 files changed

+369
-0
lines changed

Diff for: validator/client/src/main/java/tech/pegasys/teku/validator/client/restapi/ValidatorRestApi.java

+3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import tech.pegasys.teku.validator.client.restapi.apis.DeleteRemoteKeys;
4343
import tech.pegasys.teku.validator.client.restapi.apis.GetFeeRecipient;
4444
import tech.pegasys.teku.validator.client.restapi.apis.GetGasLimit;
45+
import tech.pegasys.teku.validator.client.restapi.apis.GetGraffiti;
4546
import tech.pegasys.teku.validator.client.restapi.apis.GetKeys;
4647
import tech.pegasys.teku.validator.client.restapi.apis.GetRemoteKeys;
4748
import tech.pegasys.teku.validator.client.restapi.apis.PostKeys;
@@ -57,6 +58,7 @@ public class ValidatorRestApi {
5758
public static final String TAG_KEY_MANAGEMENT = "Key Management";
5859
public static final String TAG_FEE_RECIPIENT = "Fee Recipient";
5960
public static final String TAG_GAS_LIMIT = "Gas Limit";
61+
public static final String TAG_GRAFFITI = "Graffiti";
6062

6163
public static RestApi create(
6264
final Spec spec,
@@ -128,6 +130,7 @@ public static RestApi create(
128130
.endpoint(new DeleteFeeRecipient(proposerConfigManager))
129131
.endpoint(new DeleteGasLimit(proposerConfigManager))
130132
.endpoint(new PostVoluntaryExit(voluntaryExitDataProvider))
133+
.endpoint(new GetGraffiti(keyManager))
131134
.sslCertificate(config.getRestApiKeystoreFile(), config.getRestApiKeystorePasswordFile())
132135
.passwordFilePath(validatorApiBearerFile)
133136
.build();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright Consensys Software Inc., 2024
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
14+
package tech.pegasys.teku.validator.client.restapi.apis;
15+
16+
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NOT_FOUND;
17+
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK;
18+
import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.STRING_TYPE;
19+
import static tech.pegasys.teku.validator.client.restapi.ValidatorRestApi.TAG_GRAFFITI;
20+
import static tech.pegasys.teku.validator.client.restapi.ValidatorTypes.PARAM_PUBKEY_TYPE;
21+
22+
import com.fasterxml.jackson.core.JsonProcessingException;
23+
import java.nio.charset.StandardCharsets;
24+
import java.util.Objects;
25+
import java.util.Optional;
26+
import java.util.function.Function;
27+
import org.apache.tuweni.bytes.Bytes32;
28+
import tech.pegasys.teku.bls.BLSPublicKey;
29+
import tech.pegasys.teku.infrastructure.json.types.SerializableTypeDefinition;
30+
import tech.pegasys.teku.infrastructure.restapi.endpoints.EndpointMetadata;
31+
import tech.pegasys.teku.infrastructure.restapi.endpoints.RestApiEndpoint;
32+
import tech.pegasys.teku.infrastructure.restapi.endpoints.RestApiRequest;
33+
import tech.pegasys.teku.validator.client.KeyManager;
34+
import tech.pegasys.teku.validator.client.Validator;
35+
36+
public class GetGraffiti extends RestApiEndpoint {
37+
public static final String ROUTE = "/eth/v1/validator/{pubkey}/graffiti";
38+
private final KeyManager keyManager;
39+
40+
private static final SerializableTypeDefinition<GraffitiResponse> GRAFFITI_TYPE =
41+
SerializableTypeDefinition.object(GraffitiResponse.class)
42+
.withOptionalField("pubkey", STRING_TYPE, GraffitiResponse::getPublicKey)
43+
.withField("graffiti", STRING_TYPE, GraffitiResponse::getGraffiti)
44+
.build();
45+
46+
private static final SerializableTypeDefinition<GraffitiResponse> RESPONSE_TYPE =
47+
SerializableTypeDefinition.object(GraffitiResponse.class)
48+
.name("GraffitiResponse")
49+
.withField("data", GRAFFITI_TYPE, Function.identity())
50+
.build();
51+
52+
public GetGraffiti(final KeyManager keyManager) {
53+
super(
54+
EndpointMetadata.get(ROUTE)
55+
.operationId("getGraffiti")
56+
.summary("Get Graffiti")
57+
.description(
58+
"Get the graffiti for an individual validator. If no graffiti is set explicitly, returns the process-wide default.")
59+
.tags(TAG_GRAFFITI)
60+
.withBearerAuthSecurity()
61+
.pathParam(PARAM_PUBKEY_TYPE)
62+
.response(SC_OK, "Success response", RESPONSE_TYPE)
63+
.withAuthenticationResponses()
64+
.withNotFoundResponse()
65+
.build());
66+
this.keyManager = keyManager;
67+
}
68+
69+
@Override
70+
public void handleRequest(RestApiRequest request) throws JsonProcessingException {
71+
final BLSPublicKey publicKey = request.getPathParameter(PARAM_PUBKEY_TYPE);
72+
73+
final Optional<Validator> maybeValidator = keyManager.getValidatorByPublicKey(publicKey);
74+
if (maybeValidator.isEmpty()) {
75+
request.respondError(SC_NOT_FOUND, "Validator not found");
76+
return;
77+
}
78+
79+
String graffiti = maybeValidator.get().getGraffiti().map(this::processGraffitiBytes).orElse("");
80+
request.respondOk(new GraffitiResponse(publicKey, graffiti));
81+
}
82+
83+
private String processGraffitiBytes(final Bytes32 graffiti) {
84+
return new String(graffiti.toArrayUnsafe(), StandardCharsets.UTF_8).strip().replace("\0", "");
85+
}
86+
87+
static class GraffitiResponse {
88+
private final Optional<String> publicKey;
89+
private final String graffiti;
90+
91+
GraffitiResponse(final BLSPublicKey publicKey, final String graffiti) {
92+
this.publicKey = Optional.of(publicKey.toHexString());
93+
this.graffiti = graffiti;
94+
}
95+
96+
Optional<String> getPublicKey() {
97+
return publicKey;
98+
}
99+
100+
String getGraffiti() {
101+
return graffiti;
102+
}
103+
104+
@Override
105+
public boolean equals(Object o) {
106+
if (this == o) {
107+
return true;
108+
}
109+
if (o == null || getClass() != o.getClass()) {
110+
return false;
111+
}
112+
GraffitiResponse that = (GraffitiResponse) o;
113+
return Objects.equals(publicKey, that.publicKey) && Objects.equals(graffiti, that.graffiti);
114+
}
115+
116+
@Override
117+
public int hashCode() {
118+
return Objects.hash(publicKey, graffiti);
119+
}
120+
}
121+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright Consensys Software Inc., 2024
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
14+
package tech.pegasys.teku.validator.client.restapi.apis;
15+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.mockito.ArgumentMatchers.any;
18+
import static org.mockito.ArgumentMatchers.eq;
19+
import static org.mockito.Mockito.mock;
20+
import static org.mockito.Mockito.when;
21+
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST;
22+
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_FORBIDDEN;
23+
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_INTERNAL_SERVER_ERROR;
24+
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NOT_FOUND;
25+
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK;
26+
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_UNAUTHORIZED;
27+
import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.getResponseStringFromMetadata;
28+
import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.verifyMetadataErrorResponse;
29+
30+
import com.fasterxml.jackson.core.JsonProcessingException;
31+
import java.io.IOException;
32+
import java.util.Optional;
33+
import org.apache.tuweni.bytes.Bytes32;
34+
import org.junit.jupiter.api.Test;
35+
import tech.pegasys.teku.bls.BLSPublicKey;
36+
import tech.pegasys.teku.infrastructure.http.HttpErrorResponse;
37+
import tech.pegasys.teku.infrastructure.restapi.StubRestApiRequest;
38+
import tech.pegasys.teku.spec.TestSpecFactory;
39+
import tech.pegasys.teku.spec.signatures.Signer;
40+
import tech.pegasys.teku.spec.util.DataStructureUtil;
41+
import tech.pegasys.teku.validator.api.Bytes32Parser;
42+
import tech.pegasys.teku.validator.client.OwnedKeyManager;
43+
import tech.pegasys.teku.validator.client.Validator;
44+
45+
class GetGraffitiTest {
46+
private final OwnedKeyManager keyManager = mock(OwnedKeyManager.class);
47+
private final GetGraffiti handler = new GetGraffiti(keyManager);
48+
private StubRestApiRequest request;
49+
50+
private final DataStructureUtil dataStructureUtil =
51+
new DataStructureUtil(TestSpecFactory.createDefault());
52+
53+
@Test
54+
void shouldGetGraffiti() throws JsonProcessingException {
55+
final BLSPublicKey publicKey = dataStructureUtil.randomPublicKey();
56+
final String stringGraffiti = "Test graffiti";
57+
final Bytes32 graffiti = Bytes32Parser.toBytes32(stringGraffiti);
58+
59+
request =
60+
StubRestApiRequest.builder()
61+
.metadata(handler.getMetadata())
62+
.pathParameter("pubkey", publicKey.toHexString())
63+
.build();
64+
65+
final Validator validator =
66+
new Validator(publicKey, mock(Signer.class), () -> Optional.of(graffiti));
67+
when(keyManager.getValidatorByPublicKey(eq(publicKey))).thenReturn(Optional.of(validator));
68+
69+
handler.handleRequest(request);
70+
71+
GetGraffiti.GraffitiResponse expectedResponse =
72+
new GetGraffiti.GraffitiResponse(publicKey, stringGraffiti);
73+
assertThat(request.getResponseCode()).isEqualTo(SC_OK);
74+
assertThat(request.getResponseBody()).isEqualTo(expectedResponse);
75+
}
76+
77+
@Test
78+
void shouldGetEmptyGraffiti() throws JsonProcessingException {
79+
final BLSPublicKey publicKey = dataStructureUtil.randomPublicKey();
80+
request =
81+
StubRestApiRequest.builder()
82+
.metadata(handler.getMetadata())
83+
.pathParameter("pubkey", publicKey.toHexString())
84+
.build();
85+
86+
final Validator validator = new Validator(publicKey, mock(Signer.class), Optional::empty);
87+
when(keyManager.getValidatorByPublicKey(eq(publicKey))).thenReturn(Optional.of(validator));
88+
89+
handler.handleRequest(request);
90+
91+
GetGraffiti.GraffitiResponse expectedResponse = new GetGraffiti.GraffitiResponse(publicKey, "");
92+
assertThat(request.getResponseCode()).isEqualTo(SC_OK);
93+
assertThat(request.getResponseBody()).isEqualTo(expectedResponse);
94+
}
95+
96+
@Test
97+
void shouldHandleValidatorNotFound() throws IOException {
98+
request =
99+
StubRestApiRequest.builder()
100+
.metadata(handler.getMetadata())
101+
.pathParameter("pubkey", dataStructureUtil.randomPublicKey().toHexString())
102+
.build();
103+
104+
when(keyManager.getValidatorByPublicKey(any())).thenReturn(Optional.empty());
105+
106+
handler.handleRequest(request);
107+
assertThat(request.getResponseCode()).isEqualTo(SC_NOT_FOUND);
108+
assertThat(request.getResponseBody())
109+
.isEqualTo(new HttpErrorResponse(SC_NOT_FOUND, "Validator not found"));
110+
}
111+
112+
@Test
113+
void metadata_shouldHandle200() throws JsonProcessingException {
114+
GetGraffiti.GraffitiResponse response =
115+
new GetGraffiti.GraffitiResponse(dataStructureUtil.randomPublicKey(), "Test graffiti");
116+
final String responseData = getResponseStringFromMetadata(handler, SC_OK, response);
117+
assertThat(responseData)
118+
.isEqualTo(
119+
"{\"data\":{\"pubkey\":"
120+
+ "\"0xa4654ac3105a58c7634031b5718c4880c87300f72091cfbc69fe490b71d93a671e00e80a388e1ceb8ea1de112003e976\","
121+
+ "\"graffiti\":\"Test graffiti\"}}");
122+
}
123+
124+
@Test
125+
void metadata_shouldHandle400() throws JsonProcessingException {
126+
verifyMetadataErrorResponse(handler, SC_BAD_REQUEST);
127+
}
128+
129+
@Test
130+
void metadata_shouldHandle401() throws JsonProcessingException {
131+
verifyMetadataErrorResponse(handler, SC_UNAUTHORIZED);
132+
}
133+
134+
@Test
135+
void metadata_shouldHandle403() throws JsonProcessingException {
136+
verifyMetadataErrorResponse(handler, SC_FORBIDDEN);
137+
}
138+
139+
@Test
140+
void metadata_shouldHandle500() throws JsonProcessingException {
141+
verifyMetadataErrorResponse(handler, SC_INTERNAL_SERVER_ERROR);
142+
}
143+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
{
2+
"get" : {
3+
"tags" : [ "Graffiti" ],
4+
"operationId" : "getGraffiti",
5+
"summary" : "Get Graffiti",
6+
"description" : "Get the graffiti for an individual validator. If no graffiti is set explicitly, returns the process-wide default.",
7+
"parameters" : [ {
8+
"name" : "pubkey",
9+
"required" : true,
10+
"in" : "path",
11+
"schema" : {
12+
"type" : "string",
13+
"pattern" : "^0x[a-fA-F0-9]{96}$",
14+
"example" : "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a"
15+
}
16+
} ],
17+
"security" : [ {
18+
"bearerAuth" : [ ]
19+
} ],
20+
"responses" : {
21+
"200" : {
22+
"description" : "Success response",
23+
"content" : {
24+
"application/json" : {
25+
"schema" : {
26+
"$ref" : "#/components/schemas/GraffitiResponse"
27+
}
28+
}
29+
}
30+
},
31+
"401" : {
32+
"description" : "Unauthorized, no token is found",
33+
"content" : {
34+
"application/json" : {
35+
"schema" : {
36+
"$ref" : "#/components/schemas/HttpErrorResponse"
37+
}
38+
}
39+
}
40+
},
41+
"403" : {
42+
"description" : "Forbidden, a token is found but is invalid",
43+
"content" : {
44+
"application/json" : {
45+
"schema" : {
46+
"$ref" : "#/components/schemas/HttpErrorResponse"
47+
}
48+
}
49+
}
50+
},
51+
"404" : {
52+
"description" : "Not found",
53+
"content" : {
54+
"application/json" : {
55+
"schema" : {
56+
"$ref" : "#/components/schemas/HttpErrorResponse"
57+
}
58+
}
59+
}
60+
},
61+
"400" : {
62+
"description" : "The request could not be processed, check the response for more information.",
63+
"content" : {
64+
"application/json" : {
65+
"schema" : {
66+
"$ref" : "#/components/schemas/HttpErrorResponse"
67+
}
68+
}
69+
}
70+
},
71+
"500" : {
72+
"description" : "Internal server error",
73+
"content" : {
74+
"application/json" : {
75+
"schema" : {
76+
"$ref" : "#/components/schemas/HttpErrorResponse"
77+
}
78+
}
79+
}
80+
}
81+
}
82+
}
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"title" : "GraffitiResponse",
3+
"type" : "object",
4+
"required" : [ "data" ],
5+
"properties" : {
6+
"data" : {
7+
"type" : "object",
8+
"required" : [ "graffiti" ],
9+
"properties" : {
10+
"pubkey" : {
11+
"type" : "string"
12+
},
13+
"graffiti" : {
14+
"type" : "string"
15+
}
16+
}
17+
}
18+
}
19+
}

0 commit comments

Comments
 (0)