Skip to content

Commit

Permalink
Migrate PostSyncCommittees to MigratingEndpointAdapter (Consensys#5634)
Browse files Browse the repository at this point in the history
  • Loading branch information
courtneyeh authored Jun 1, 2022
1 parent 051e143 commit 2a71c8a
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
import tech.pegasys.teku.validator.api.SubmitDataError;

public class PostSyncCommitteesIntegrationTest extends AbstractDataBackedRestAPIIntegrationTest {
private final String errorString = "The Error Description";
private static final String ERROR_MESSAGE = "The Error Description";

@Test
void shouldSubmitSyncCommitteesAndGetResponse() throws IOException {
Expand All @@ -53,7 +53,7 @@ void shouldSubmitSyncCommitteesAndGetResponse() throws IOException {
dataStructureUtil.randomUInt64(),
new BLSSignature(dataStructureUtil.randomSignature())));
final SafeFuture<List<SubmitDataError>> future =
SafeFuture.completedFuture(List.of(new SubmitDataError(UInt64.ZERO, errorString)));
SafeFuture.completedFuture(List.of(new SubmitDataError(UInt64.ZERO, ERROR_MESSAGE)));
when(validatorApiChannel.sendSyncCommitteeMessages(
requestBody.get(0).asInternalCommitteeSignature(spec).stream()
.collect(Collectors.toList())))
Expand All @@ -64,7 +64,7 @@ void shouldSubmitSyncCommitteesAndGetResponse() throws IOException {
final PostDataFailureResponse responseBody =
jsonProvider.jsonToObject(response.body().string(), PostDataFailureResponse.class);

assertThat(responseBody.failures.get(0).message).isEqualTo(errorString);
assertThat(responseBody.failures.get(0).message).isEqualTo(ERROR_MESSAGE);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"post" : {
"tags" : [ "Beacon", "Validator Required Api" ],
"operationId" : "postSyncCommittees",
"summary" : "Submit sync committee messages to node",
"description" : "Submits sync committee message objects to the node.\n\nSync committee messages are not present in phase0, but are required for Altair networks.\n\nIf a sync committee message is validated successfully the node MUST publish that sync committee message on all applicable subnets.\n\nIf one or more sync committee messages fail validation the node MUST return a 400 error with details of which sync committee messages have failed, and why.",
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/SyncCommitteeMessage"
}
}
}
}
},
"responses" : {
"200" : {
"description" : "Sync committee signatures are stored in pool and broadcast on appropriate subnet",
"content" : { }
},
"400" : {
"description" : "Errors with one or more sync committee signatures",
"content" : {
"application/json" : {
"schema" : {
"title" : "BadRequestResponses",
"type" : "object",
"oneOf" : [ {
"$ref" : "#/components/schemas/PostDataFailureResponse"
}, {
"$ref" : "#/components/schemas/HttpErrorResponse"
} ]
}
}
}
},
"500" : {
"description" : "Internal server error",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/HttpErrorResponse"
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"title" : "SyncCommitteeMessage",
"type" : "object",
"required" : [ "slot", "beacon_block_root", "validator_index", "signature" ],
"properties" : {
"slot" : {
"type" : "string",
"description" : "unsigned 64 bit integer",
"example" : "1",
"format" : "uint64"
},
"beacon_block_root" : {
"type" : "string",
"description" : "Bytes32 hexadecimal",
"example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
"format" : "byte"
},
"validator_index" : {
"type" : "string",
"description" : "unsigned 64 bit integer",
"example" : "1",
"format" : "uint64"
},
"signature" : {
"type" : "string",
"pattern" : "^0x[a-fA-F0-9]{2,}$",
"description" : "SSZ hexadecimal",
"format" : "bytes"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ private void addBeaconHandlers(final DataProvider dataProvider, final Spec spec)
addMigratedEndpoint(new PostProposerSlashing(dataProvider));
addMigratedEndpoint(new GetVoluntaryExits(dataProvider));
addMigratedEndpoint(new PostVoluntaryExit(dataProvider));
app.post(PostSyncCommittees.ROUTE, new PostSyncCommittees(dataProvider, jsonProvider));
addMigratedEndpoint(new PostSyncCommittees(dataProvider));
app.post(PostValidatorLiveness.ROUTE, new PostValidatorLiveness(dataProvider, jsonProvider));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,83 @@
package tech.pegasys.teku.beaconrestapi.handlers.v1.beacon;

import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.RES_BAD_REQUEST;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.RES_INTERNAL_ERROR;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.RES_OK;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_BEACON;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_VALIDATOR_REQUIRED;
import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.HTTP_ERROR_RESPONSE_TYPE;
import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.INTEGER_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 io.javalin.http.Context;
import io.javalin.plugin.openapi.annotations.HttpMethod;
import io.javalin.plugin.openapi.annotations.OpenApi;
import io.javalin.plugin.openapi.annotations.OpenApiContent;
import io.javalin.plugin.openapi.annotations.OpenApiRequestBody;
import io.javalin.plugin.openapi.annotations.OpenApiResponse;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import org.jetbrains.annotations.NotNull;
import tech.pegasys.teku.api.DataProvider;
import tech.pegasys.teku.api.ValidatorDataProvider;
import tech.pegasys.teku.api.response.v1.beacon.PostDataFailureResponse;
import tech.pegasys.teku.api.schema.altair.SyncCommitteeMessage;
import tech.pegasys.teku.beaconrestapi.handlers.AbstractHandler;
import tech.pegasys.teku.beaconrestapi.schema.BadRequest;
import tech.pegasys.teku.beaconrestapi.MigratingEndpointAdapter;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.provider.JsonProvider;
import tech.pegasys.teku.infrastructure.http.HttpErrorResponse;
import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition;
import tech.pegasys.teku.infrastructure.json.types.SerializableOneOfTypeDefinition;
import tech.pegasys.teku.infrastructure.json.types.SerializableOneOfTypeDefinitionBuilder;
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.RestApiRequest;
import tech.pegasys.teku.spec.datastructures.operations.versions.altair.SyncCommitteeMessage;
import tech.pegasys.teku.spec.datastructures.operations.versions.altair.SyncCommitteeMessageSchema;
import tech.pegasys.teku.validator.api.SubmitDataError;

public class PostSyncCommittees extends AbstractHandler {
public class PostSyncCommittees extends MigratingEndpointAdapter {
public static final String ROUTE = "/eth/v1/beacon/pool/sync_committees";
private final ValidatorDataProvider provider;

public PostSyncCommittees(final DataProvider provider, final JsonProvider jsonProvider) {
this(provider.getValidatorDataProvider(), jsonProvider);
private static final SerializableTypeDefinition<List<SubmitDataError>> BAD_REQUEST_RESPONSE =
SerializableTypeDefinition.<List<SubmitDataError>>object()
.name("PostDataFailureResponse")
.withField("code", INTEGER_TYPE, (__) -> SC_BAD_REQUEST)
.withField("message", STRING_TYPE, (__) -> "some failures")
.withField(
"failures", listOf(SubmitDataError.getJsonTypeDefinition()), Function.identity())
.build();

public PostSyncCommittees(final DataProvider provider) {
this(provider.getValidatorDataProvider());
}

public PostSyncCommittees(final ValidatorDataProvider provider, final JsonProvider jsonProvider) {
super(jsonProvider);
public PostSyncCommittees(final ValidatorDataProvider provider) {
super(
EndpointMetadata.post(ROUTE)
.operationId("postSyncCommittees")
.summary("Submit sync committee messages to node")
.description(
"Submits sync committee message objects to the node.\n\n"
+ "Sync committee messages are not present in phase0, but are required for Altair networks.\n\n"
+ "If a sync committee message is validated successfully the node MUST publish that sync committee message on all applicable subnets.\n\n"
+ "If one or more sync committee messages fail validation the node MUST return a 400 error with details of which sync committee messages have failed, and why.")
.tags(TAG_BEACON, TAG_VALIDATOR_REQUIRED)
.requestBodyType(
DeserializableTypeDefinition.listOf(
SyncCommitteeMessageSchema.INSTANCE.getJsonTypeDefinition()))
.response(
SC_OK,
"Sync committee signatures are stored in pool and broadcast on appropriate subnet")
.response(
SC_BAD_REQUEST,
"Errors with one or more sync committee signatures",
getBadRequestResponseTypes())
.build());
this.provider = provider;
}

Expand All @@ -58,7 +101,11 @@ public PostSyncCommittees(final ValidatorDataProvider provider, final JsonProvid
tags = {TAG_BEACON, TAG_VALIDATOR_REQUIRED},
requestBody =
@OpenApiRequestBody(
content = {@OpenApiContent(from = SyncCommitteeMessage.class, isArray = true)}),
content = {
@OpenApiContent(
from = tech.pegasys.teku.api.schema.altair.SyncCommitteeMessage.class,
isArray = true)
}),
description =
"Submits sync committee message objects to the node.\n\n"
+ "Sync committee messages are not present in phase0, but are required for Altair networks.\n\n"
Expand All @@ -75,18 +122,30 @@ public PostSyncCommittees(final ValidatorDataProvider provider, final JsonProvid
@OpenApiResponse(status = RES_INTERNAL_ERROR)
})
@Override
public void handle(final Context ctx) throws Exception {
try {
final List<SyncCommitteeMessage> messages =
Arrays.asList(parseRequestBody(ctx.body(), SyncCommitteeMessage[].class));
final SafeFuture<Optional<PostDataFailureResponse>> future =
provider.submitCommitteeSignatures(messages);
public void handle(@NotNull final Context ctx) throws Exception {
adapt(ctx);
}

handlePostDataResult(ctx, future);
@Override
public void handleRequest(RestApiRequest request) throws JsonProcessingException {
final List<SyncCommitteeMessage> messages = request.getRequestBody();
final SafeFuture<List<SubmitDataError>> future = provider.submitCommitteeSignatures(messages);

request.respondAsync(
future.thenApply(
submitDataErrorList -> {
if (submitDataErrorList.isEmpty()) {
return AsyncApiResponse.respondWithCode(SC_OK);
}
return AsyncApiResponse.respondWithObject(SC_BAD_REQUEST, submitDataErrorList);
}));
}

} catch (final IllegalArgumentException e) {
ctx.json(BadRequest.badRequest(jsonProvider, e.getMessage()));
ctx.status(SC_BAD_REQUEST);
}
private static SerializableOneOfTypeDefinition<Object> getBadRequestResponseTypes() {
final SerializableOneOfTypeDefinitionBuilder<Object> builder =
new SerializableOneOfTypeDefinitionBuilder<>().title("BadRequestResponses");
builder.withType(value -> value instanceof List, BAD_REQUEST_RESPONSE);
builder.withType(value -> value instanceof HttpErrorResponse, HTTP_ERROR_RESPONSE_TYPE);
return builder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public abstract class AbstractMigratedBeaconHandlerTest {
protected final SyncDataProvider syncDataProvider =
new SyncDataProvider(syncService, rejectedExecutionSupplier);
protected final SchemaDefinitionCache schemaDefinitionCache = new SchemaDefinitionCache(spec);
protected final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec);
protected DataStructureUtil dataStructureUtil = new DataStructureUtil(spec);

protected ChainDataProvider chainDataProvider = mock(ChainDataProvider.class);
protected final ValidatorDataProvider validatorDataProvider = mock(ValidatorDataProvider.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright 2022 ConsenSys AG.
*
* 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 javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_INTERNAL_SERVER_ERROR;
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.List;
import org.assertj.core.api.AssertionsForClassTypes;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import tech.pegasys.teku.beaconrestapi.AbstractMigratedBeaconHandlerTest;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.infrastructure.http.HttpStatusCodes;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
import tech.pegasys.teku.spec.TestSpecFactory;
import tech.pegasys.teku.spec.util.DataStructureUtil;
import tech.pegasys.teku.validator.api.SubmitDataError;

public class PostSyncCommitteesTest extends AbstractMigratedBeaconHandlerTest {

@BeforeEach
void setup() {
spec = TestSpecFactory.createMinimalAltair();
dataStructureUtil = new DataStructureUtil(spec);
setHandler(new PostSyncCommittees(validatorDataProvider));
}

@Test
void shouldBeAbleToSubmitSyncCommittees() throws Exception {
request.setRequestBody(List.of(dataStructureUtil.randomSyncCommitteeMessage()));
when(validatorDataProvider.submitCommitteeSignatures(any()))
.thenReturn(SafeFuture.completedFuture(List.of()));

handler.handleRequest(request);

assertThat(request.getResponseCode()).isEqualTo(SC_OK);
assertThat(request.getResponseBody()).isNull();
}

@Test
void shouldReportInvalidSyncCommittees() throws Exception {
final List<SubmitDataError> errors = List.of(new SubmitDataError(UInt64.ZERO, "Darn"));
request.setRequestBody(List.of(dataStructureUtil.randomSyncCommitteeMessage()));
when(validatorDataProvider.submitCommitteeSignatures(any()))
.thenReturn(SafeFuture.completedFuture(errors));

handler.handleRequest(request);

assertThat(request.getResponseCode()).isEqualTo(SC_BAD_REQUEST);
assertThat(request.getResponseBody()).isEqualTo(errors);
}

@Test
void metadata_shouldHandle400_errorResponse() throws IOException {
List<SubmitDataError> responseData =
List.of(
new SubmitDataError(UInt64.ZERO, "Darn"), new SubmitDataError(UInt64.ONE, "Incorrect"));

final String data =
getResponseStringFromMetadata(handler, HttpStatusCodes.SC_BAD_REQUEST, responseData);
final String expected =
Resources.toString(
Resources.getResource(PostSyncCommitteesTest.class, "postSyncCommittees.json"), UTF_8);
AssertionsForClassTypes.assertThat(data).isEqualTo(expected);
}

@Test
void metadata_shouldHandle400() throws JsonProcessingException {
verifyMetadataErrorResponse(handler, SC_BAD_REQUEST);
}

@Test
void metadata_shouldHandle500() throws JsonProcessingException {
verifyMetadataErrorResponse(handler, SC_INTERNAL_SERVER_ERROR);
}

@Test
void metadata_shouldHandle200() throws IOException {
verifyMetadataEmptyResponse(handler, SC_OK);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"code":400,"message":"some failures","failures":[{"index":"0","message":"Darn"},{"index":"1","message":"Incorrect"}]}
Loading

0 comments on commit 2a71c8a

Please sign in to comment.