Skip to content

Commit

Permalink
Add support for phase0 and electra in PostAttesterSlashingV2 (#8487)
Browse files Browse the repository at this point in the history
* support schema for oneOf electra and phase0

Signed-off-by: Gabriel Fukushima <[email protected]>

* fix tests

Signed-off-by: Gabriel Fukushima <[email protected]>

---------

Signed-off-by: Gabriel Fukushima <[email protected]>
  • Loading branch information
gfukushima authored Aug 19, 2024
1 parent b7eea54 commit cde06e1
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,45 +21,58 @@
import static tech.pegasys.teku.spec.SpecMilestone.ELECTRA;
import static tech.pegasys.teku.spec.SpecMilestone.PHASE0;

import com.fasterxml.jackson.databind.JsonNode;
import java.util.Collections;
import java.util.Locale;
import java.util.Optional;
import okhttp3.Response;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestTemplate;
import tech.pegasys.teku.api.schema.AttesterSlashing;
import tech.pegasys.teku.beaconrestapi.AbstractDataBackedRestAPIIntegrationTest;
import tech.pegasys.teku.beaconrestapi.handlers.v2.beacon.PostAttesterSlashingV2;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.infrastructure.json.JsonTestUtil;
import tech.pegasys.teku.spec.SpecMilestone;
import tech.pegasys.teku.spec.TestSpecContext;
import tech.pegasys.teku.spec.TestSpecFactory;
import tech.pegasys.teku.spec.TestSpecInvocationContextProvider;
import tech.pegasys.teku.spec.util.DataStructureUtil;
import tech.pegasys.teku.statetransition.validation.InternalValidationResult;

@TestSpecContext(milestone = {PHASE0, ELECTRA})
public class PostAttesterSlashingV2IntegrationTest
extends AbstractDataBackedRestAPIIntegrationTest {

private final DataStructureUtil dataStructureUtil =
new DataStructureUtil(TestSpecFactory.createMinimalElectra());
private DataStructureUtil dataStructureUtil;
private SpecMilestone specMilestone;

@BeforeEach
public void setup() {
startRestAPIAtGenesis();
void setup(final TestSpecInvocationContextProvider.SpecContext specContext) {
spec = specContext.getSpec();
specMilestone = specContext.getSpecMilestone();
startRestAPIAtGenesis(specMilestone);
dataStructureUtil = specContext.getDataStructureUtil();
}

@Test
@TestTemplate
public void shouldReturnBadRequestWhenRequestBodyIsEmpty() throws Exception {
final Response response = post(PostAttesterSlashingV2.ROUTE, jsonProvider.objectToJSON(""));
Assertions.assertThat(response.code()).isEqualTo(SC_BAD_REQUEST);
}

@Test
@TestTemplate
public void shouldReturnBadRequestWhenRequestBodyIsInvalid() throws Exception {
final Response response =
post(PostAttesterSlashingV2.ROUTE, jsonProvider.objectToJSON("{\"foo\": \"bar\"}"));
post(
PostAttesterSlashingV2.ROUTE,
jsonProvider.objectToJSON("{\"foo\": \"bar\"}"),
Collections.emptyMap(),
Optional.of(specMilestone.name().toLowerCase(Locale.ROOT)));
assertThat(response.code()).isEqualTo(400);
}

@Test
@TestTemplate
public void shouldReturnServerErrorWhenUnexpectedErrorHappens() throws Exception {
final tech.pegasys.teku.spec.datastructures.operations.AttesterSlashing slashing =
dataStructureUtil.randomAttesterSlashing();
Expand All @@ -69,11 +82,15 @@ public void shouldReturnServerErrorWhenUnexpectedErrorHappens() throws Exception
doThrow(new RuntimeException()).when(attesterSlashingPool).addLocal(slashing);

final Response response =
post(PostAttesterSlashingV2.ROUTE, jsonProvider.objectToJSON(schemaSlashing));
post(
PostAttesterSlashingV2.ROUTE,
jsonProvider.objectToJSON(schemaSlashing),
Collections.emptyMap(),
Optional.of(specMilestone.name().toLowerCase(Locale.ROOT)));
assertThat(response.code()).isEqualTo(500);
}

@Test
@TestTemplate
public void shouldReturnSuccessWhenRequestBodyIsValid() throws Exception {
final tech.pegasys.teku.spec.datastructures.operations.AttesterSlashing slashing =
dataStructureUtil.randomAttesterSlashing();
Expand All @@ -84,10 +101,56 @@ public void shouldReturnSuccessWhenRequestBodyIsValid() throws Exception {
.thenReturn(SafeFuture.completedFuture(InternalValidationResult.ACCEPT));

final Response response =
post(PostAttesterSlashingV2.ROUTE, jsonProvider.objectToJSON(schemaSlashing));
post(
PostAttesterSlashingV2.ROUTE,
jsonProvider.objectToJSON(schemaSlashing),
Collections.emptyMap(),
Optional.of(specMilestone.name().toLowerCase(Locale.ROOT)));

verify(attesterSlashingPool).addLocal(slashing);

assertThat(response.code()).isEqualTo(200);
}

@TestTemplate
void shouldFailWhenMissingConsensusHeader() throws Exception {
final tech.pegasys.teku.spec.datastructures.operations.AttesterSlashing slashing =
dataStructureUtil.randomAttesterSlashing();

final AttesterSlashing schemaSlashing = new AttesterSlashing(slashing);

final Response response =
post(PostAttesterSlashingV2.ROUTE, jsonProvider.objectToJSON(schemaSlashing));

assertThat(response.code()).isEqualTo(SC_BAD_REQUEST);

final JsonNode resultAsJsonNode = JsonTestUtil.parseAsJsonNode(response.body().string());
assertThat(resultAsJsonNode.get("message").asText())
.isEqualTo("(Eth-Consensus-Version) header value was unexpected");
}

@TestTemplate
void shouldFailWhenBadConsensusHeaderValue() throws Exception {

final tech.pegasys.teku.spec.datastructures.operations.AttesterSlashing slashing =
dataStructureUtil.randomAttesterSlashing();

final AttesterSlashing schemaSlashing = new AttesterSlashing(slashing);

when(attesterSlashingPool.addLocal(slashing))
.thenReturn(SafeFuture.completedFuture(InternalValidationResult.ACCEPT));

final Response response =
post(
PostAttesterSlashingV2.ROUTE,
jsonProvider.objectToJSON(schemaSlashing),
Collections.emptyMap(),
Optional.of("NonExistingMileStone"));

assertThat(response.code()).isEqualTo(SC_BAD_REQUEST);

final JsonNode resultAsJsonNode = JsonTestUtil.parseAsJsonNode(response.body().string());
assertThat(resultAsJsonNode.get("message").asText())
.isEqualTo("(Eth-Consensus-Version) header value was unexpected");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/AttesterSlashingElectra"
"type" : "object",
"oneOf" : [ {
"$ref" : "#/components/schemas/AttesterSlashingElectra"
}, {
"$ref" : "#/components/schemas/AttesterSlashingPhase0"
} ]
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,34 @@
package tech.pegasys.teku.beaconrestapi.handlers.v2.beacon;

import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.ETH_CONSENSUS_VERSION_TYPE;
import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil.headerBasedSelector;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_CONSENSUS_VERSION;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_BEACON;

import com.fasterxml.jackson.core.JsonProcessingException;
import java.util.function.Predicate;
import tech.pegasys.teku.api.DataProvider;
import tech.pegasys.teku.api.NodeDataProvider;
import tech.pegasys.teku.api.schema.Version;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
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.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.infrastructure.restapi.openapi.request.OneOfJsonRequestContentTypeDefinition;
import tech.pegasys.teku.spec.SpecMilestone;
import tech.pegasys.teku.spec.datastructures.operations.AttesterSlashing;
import tech.pegasys.teku.spec.schemas.SchemaDefinitionCache;
import tech.pegasys.teku.spec.schemas.SchemaDefinitions;
import tech.pegasys.teku.statetransition.validation.InternalValidationResult;
import tech.pegasys.teku.statetransition.validation.ValidationResultCode;

public class PostAttesterSlashingV2 extends RestApiEndpoint {
public static final String ROUTE = "/eth/v2/beacon/pool/attester_slashings";
private final NodeDataProvider nodeDataProvider;
private final SchemaDefinitionCache schemaDefinitionCache;

public PostAttesterSlashingV2(
final DataProvider dataProvider, final SchemaDefinitionCache schemaDefinitionCache) {
Expand All @@ -47,32 +50,64 @@ public PostAttesterSlashingV2(

public PostAttesterSlashingV2(
final NodeDataProvider provider, final SchemaDefinitionCache schemaDefinitionCache) {
super(
EndpointMetadata.post(ROUTE)
.operationId("submitPoolAttesterSlashingsV2")
.summary("Submit AttesterSlashing object to node's pool")
.description(
"Submits AttesterSlashing object to node's pool. Upon successful validation the node MUST broadcast it to network.")
.tags(TAG_BEACON)
.requestBodyType(getRequestType(schemaDefinitionCache))
.headerRequired(
ETH_CONSENSUS_VERSION_TYPE.withDescription(
"Version of the attester slashing being submitted."))
.response(SC_OK, "Success")
.build());
super(createMetadata(schemaDefinitionCache));
this.nodeDataProvider = provider;
this.schemaDefinitionCache = schemaDefinitionCache;
}

private static EndpointMetadata createMetadata(
final SchemaDefinitionCache schemaDefinitionCache) {

final DeserializableTypeDefinition<AttesterSlashing> attesterSlashingPhase0Schema =
schemaDefinitionCache
.getSchemaDefinition(SpecMilestone.PHASE0)
.getAttesterSlashingSchema()
.getJsonTypeDefinition();

final DeserializableTypeDefinition<AttesterSlashing> attesterSlashingElectraSchema =
schemaDefinitionCache
.getSchemaDefinition(SpecMilestone.ELECTRA)
.getAttesterSlashingSchema()
.getJsonTypeDefinition();

final SerializableOneOfTypeDefinition<AttesterSlashing> attesterSlashingSchemaDefinition =
new SerializableOneOfTypeDefinitionBuilder<AttesterSlashing>()
.withType(
electraAttesterSlashingsPredicate(schemaDefinitionCache),
attesterSlashingElectraSchema)
.withType(
phase0AttesterSlashingsPredicate(schemaDefinitionCache),
attesterSlashingPhase0Schema)
.build();

final OneOfJsonRequestContentTypeDefinition.BodyTypeSelector<AttesterSlashing>
attesterSlashingBodySelector =
context ->
headerBasedSelector(
context.getHeaders(),
schemaDefinitionCache,
SchemaDefinitions::getAttesterSlashingSchema);

return EndpointMetadata.post(ROUTE)
.operationId("submitPoolAttesterSlashingsV2")
.summary("Submit AttesterSlashing object to node's pool")
.description(
"Submits AttesterSlashing object to node's pool. Upon successful validation the node MUST broadcast it to network.")
.tags(TAG_BEACON)
.requestBodyType(attesterSlashingSchemaDefinition, attesterSlashingBodySelector)
.headerRequired(
ETH_CONSENSUS_VERSION_TYPE.withDescription(
"Version of the attester slashing being submitted."))
.response(SC_OK, "Success")
.build();
}

@Override
public void handleRequest(final RestApiRequest request) throws JsonProcessingException {
handleAttesterSlashingRequest(request, nodeDataProvider, schemaDefinitionCache);
handleAttesterSlashingRequest(request, nodeDataProvider);
}

public static void handleAttesterSlashingRequest(
final RestApiRequest request,
final NodeDataProvider nodeDataProvider,
final SchemaDefinitionCache schemaDefinitionCache)
final RestApiRequest request, final NodeDataProvider nodeDataProvider)
throws JsonProcessingException {
final AttesterSlashing attesterSlashing = request.getRequestBody();
final SafeFuture<InternalValidationResult> future =
Expand All @@ -90,30 +125,25 @@ public static void handleAttesterSlashingRequest(
.orElse(
"Invalid attester slashing, it will never pass validation so it's rejected."));
} else {
request.header(
HEADER_CONSENSUS_VERSION,
Version.fromMilestone(
getMilestoneFromAttesterSlashing(
attesterSlashing, schemaDefinitionCache))
.name());

return AsyncApiResponse.respondWithCode(SC_OK);
}
}));
}

private static SpecMilestone getMilestoneFromAttesterSlashing(
final AttesterSlashing attesterSlashing, final SchemaDefinitionCache schemaDefinitionCache) {
return schemaDefinitionCache.milestoneAtSlot(
attesterSlashing.getAttestation1().getData().getSlot());
}

private static DeserializableTypeDefinition<AttesterSlashing> getRequestType(
private static Predicate<AttesterSlashing> electraAttesterSlashingsPredicate(
final SchemaDefinitionCache schemaDefinitionCache) {
final AttesterSlashing.AttesterSlashingSchema attesterSlashingSchema =
return attesterSlashings ->
schemaDefinitionCache
.getSchemaDefinition(SpecMilestone.ELECTRA)
.getAttesterSlashingSchema();
.milestoneAtSlot(attesterSlashings.getAttestation1().getData().getSlot())
.isGreaterThanOrEqualTo(SpecMilestone.ELECTRA);
}

return attesterSlashingSchema.getJsonTypeDefinition();
private static Predicate<AttesterSlashing> phase0AttesterSlashingsPredicate(
final SchemaDefinitionCache schemaDefinitionCache) {
return attesterSlashings ->
!schemaDefinitionCache
.milestoneAtSlot(attesterSlashings.getAttestation1().getData().getSlot())
.isGreaterThanOrEqualTo(SpecMilestone.ELECTRA);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.ETH_CONSENSUS_VERSION_TYPE;
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_OK;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_CONSENSUS_VERSION;
import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.getRequestBodyFromMetadata;
import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.verifyMetadataEmptyResponse;
import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.verifyMetadataErrorResponse;
Expand All @@ -29,9 +29,9 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.io.Resources;
import java.io.IOException;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import tech.pegasys.teku.api.schema.Version;
import tech.pegasys.teku.beaconrestapi.AbstractMigratedBeaconHandlerTest;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.infrastructure.http.HttpErrorResponse;
Expand Down Expand Up @@ -70,21 +70,6 @@ void shouldBeAbleToSubmitSlashing() throws Exception {
assertThat(request.getResponseBody()).isNull();
}

@TestTemplate
void successfulResponseShouldContainVersionInTheHeader() throws Exception {
final AttesterSlashing slashing = dataStructureUtil.randomAttesterSlashing();
request.setRequestBody(slashing);

when(nodeDataProvider.postAttesterSlashing(slashing))
.thenReturn(SafeFuture.completedFuture(InternalValidationResult.ACCEPT));

handler.handleRequest(request);

assertThat(request.getResponseCode()).isEqualTo(SC_OK);
assertThat(request.getResponseHeaders(ETH_CONSENSUS_VERSION_TYPE.getName()))
.isEqualTo(Version.fromMilestone(specMilestone).name());
}

@TestTemplate
void shouldReturnBadRequestIfAttesterSlashingIsInvalid() throws Exception {
final AttesterSlashing slashing = dataStructureUtil.randomAttesterSlashing();
Expand All @@ -107,7 +92,10 @@ void shouldReadRequestBody() throws IOException {
Resources.getResource(
PostAttesterSlashingV2Test.class, "postAttesterSlashingRequestBody.json"),
UTF_8);
assertThat(getRequestBodyFromMetadata(handler, data)).isInstanceOf(AttesterSlashing.class);
assertThat(
getRequestBodyFromMetadata(
handler, Map.of(HEADER_CONSENSUS_VERSION, specMilestone.name()), data))
.isInstanceOf(AttesterSlashing.class);
}

@TestTemplate
Expand Down

0 comments on commit cde06e1

Please sign in to comment.