Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for phase0 and electra in PostAttesterSlashingV2 #8487

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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