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

8424 use attestations v2 #8457

Closed
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
import tech.pegasys.teku.networking.eth2.gossip.subnets.AttestationTopicSubscriber;
import tech.pegasys.teku.networking.eth2.gossip.subnets.SyncCommitteeSubscriptionManager;
import tech.pegasys.teku.spec.Spec;
import tech.pegasys.teku.spec.SpecMilestone;
import tech.pegasys.teku.spec.SpecVersion;
import tech.pegasys.teku.spec.datastructures.attestation.ValidatableAttestation;
import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlock;
Expand Down Expand Up @@ -576,13 +577,21 @@ public SafeFuture<Void> subscribeToPersistentSubnets(
() -> attestationTopicSubscriber.subscribeToPersistentSubnets(subnetSubscriptions));
}

@Deprecated
@Override
public SafeFuture<List<SubmitDataError>> sendSignedAttestations(
final List<Attestation> attestations) {
return SafeFuture.collectAll(attestations.stream().map(this::processAttestation))
.thenApply(this::convertAttestationProcessingResultsToErrorList);
}

@Override
public SafeFuture<List<SubmitDataError>> sendSignedAttestationsV2(
final SpecMilestone specMilestone, final List<Attestation> attestations) {
return SafeFuture.collectAll(attestations.stream().map(this::processAttestation))
.thenApply(this::convertAttestationProcessingResultsToErrorList);
}

private SafeFuture<InternalValidationResult> processAttestation(final Attestation attestation) {
return attestationManager
.addAttestation(ValidatableAttestation.fromValidator(spec, attestation), Optional.empty())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,20 @@ public void sendSignedAttestations_shouldAddAttestationToAttestationManager() {
.addAttestation(ValidatableAttestation.from(spec, attestation), Optional.empty());
}

@Test
public void sendSignedAttestations_shouldAddAttestationToAttestationManager_V2() {
final Attestation attestation = dataStructureUtil.randomAttestation();
when(attestationManager.addAttestation(any(ValidatableAttestation.class), any()))
.thenReturn(completedFuture(InternalValidationResult.ACCEPT));
final SafeFuture<List<SubmitDataError>> result =
validatorApiHandler.sendSignedAttestationsV2(
spec.atSlot(epochStartSlot).getMilestone(), List.of(attestation));
assertThat(result).isCompletedWithValue(emptyList());

verify(attestationManager)
.addAttestation(ValidatableAttestation.from(spec, attestation), Optional.empty());
}

@Test
void sendSignedAttestations_shouldAddToDutyMetricsAndPerformanceTrackerWhenNotInvalid() {
final Attestation attestation = dataStructureUtil.randomAttestation();
Expand All @@ -818,6 +832,21 @@ void sendSignedAttestations_shouldAddToDutyMetricsAndPerformanceTrackerWhenNotIn
verify(performanceTracker).saveProducedAttestation(attestation);
}

@Test
void sendSignedAttestations_shouldAddToDutyMetricsAndPerformanceTrackerWhenNotInvalidV2() {
final Attestation attestation = dataStructureUtil.randomAttestation();
when(attestationManager.addAttestation(any(ValidatableAttestation.class), any()))
.thenReturn(completedFuture(InternalValidationResult.SAVE_FOR_FUTURE));

final SafeFuture<List<SubmitDataError>> result =
validatorApiHandler.sendSignedAttestationsV2(
spec.atSlot(epochStartSlot).getMilestone(), List.of(attestation));
assertThat(result).isCompletedWithValue(emptyList());

verify(dutyMetrics).onAttestationPublished(attestation.getData().getSlot());
verify(performanceTracker).saveProducedAttestation(attestation);
}

@Test
void sendSignedAttestations_shouldNotAddToDutyMetricsAndPerformanceTrackerWhenInvalid() {
final Attestation attestation = dataStructureUtil.randomAttestation();
Expand All @@ -832,6 +861,21 @@ void sendSignedAttestations_shouldNotAddToDutyMetricsAndPerformanceTrackerWhenIn
verify(performanceTracker, never()).saveProducedAttestation(attestation);
}

@Test
void sendSignedAttestations_shouldNotAddToDutyMetricsAndPerformanceTrackerWhenInvalid_V2() {
final Attestation attestation = dataStructureUtil.randomAttestation();
when(attestationManager.addAttestation(any(ValidatableAttestation.class), any()))
.thenReturn(completedFuture(InternalValidationResult.reject("Bad juju")));

final SafeFuture<List<SubmitDataError>> result =
validatorApiHandler.sendSignedAttestationsV2(
spec.atSlot(epochStartSlot).getMilestone(), List.of(attestation));
assertThat(result).isCompletedWithValue(List.of(new SubmitDataError(ZERO, "Bad juju")));

verify(dutyMetrics, never()).onAttestationPublished(attestation.getData().getSlot());
verify(performanceTracker, never()).saveProducedAttestation(attestation);
}

@Test
void sendSignedAttestations_shouldProcessMixOfValidAndInvalidAttestations() {
final Attestation invalidAttestation = dataStructureUtil.randomAttestation();
Expand All @@ -851,6 +895,27 @@ void sendSignedAttestations_shouldProcessMixOfValidAndInvalidAttestations() {
verify(performanceTracker).saveProducedAttestation(validAttestation);
}

@Test
void sendSignedAttestations_shouldProcessMixOfValidAndInvalidAttestations_V2() {
final Attestation invalidAttestation = dataStructureUtil.randomAttestation();
final Attestation validAttestation = dataStructureUtil.randomAttestation();
when(attestationManager.addAttestation(validatableAttestationOf(invalidAttestation), any()))
.thenReturn(completedFuture(InternalValidationResult.reject("Bad juju")));
when(attestationManager.addAttestation(validatableAttestationOf(validAttestation), any()))
.thenReturn(completedFuture(InternalValidationResult.ACCEPT));

final SafeFuture<List<SubmitDataError>> result =
validatorApiHandler.sendSignedAttestationsV2(
spec.atSlot(epochStartSlot).getMilestone(),
List.of(invalidAttestation, validAttestation));
assertThat(result).isCompletedWithValue(List.of(new SubmitDataError(ZERO, "Bad juju")));

verify(dutyMetrics, never()).onAttestationPublished(invalidAttestation.getData().getSlot());
verify(dutyMetrics).onAttestationPublished(validAttestation.getData().getSlot());
verify(performanceTracker, never()).saveProducedAttestation(invalidAttestation);
verify(performanceTracker).saveProducedAttestation(validAttestation);
}

private ValidatableAttestation validatableAttestationOf(final Attestation validAttestation) {
return argThat(
argument -> argument != null && argument.getAttestation().equals(validAttestation));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* Copyright Consensys Software Inc., 2024
*
* 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.v2.beacon;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
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_OK;

import com.fasterxml.jackson.databind.JsonNode;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import okhttp3.Response;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import tech.pegasys.teku.beaconrestapi.AbstractDataBackedRestAPIIntegrationTest;
import tech.pegasys.teku.beaconrestapi.handlers.v2.beacon.PostAttestationsV2;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.infrastructure.json.JsonTestUtil;
import tech.pegasys.teku.infrastructure.json.JsonUtil;
import tech.pegasys.teku.infrastructure.json.types.SerializableTypeDefinition;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
import tech.pegasys.teku.spec.SpecMilestone;
import tech.pegasys.teku.spec.TestSpecContext;
import tech.pegasys.teku.spec.TestSpecInvocationContextProvider;
import tech.pegasys.teku.spec.datastructures.operations.Attestation;
import tech.pegasys.teku.spec.util.DataStructureUtil;
import tech.pegasys.teku.validator.api.SubmitDataError;

@TestSpecContext(milestone = {SpecMilestone.PHASE0, SpecMilestone.ELECTRA})
public class PostAttestationsV2IntegrationTest extends AbstractDataBackedRestAPIIntegrationTest {

private DataStructureUtil dataStructureUtil;
private SpecMilestone specMilestone;

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

@TestTemplate
void shouldPostAttestations_NoErrors() throws Exception {
final List<Attestation> attestations =
List.of(dataStructureUtil.randomAttestation(), dataStructureUtil.randomAttestation());

when(validatorApiChannel.sendSignedAttestationsV2(specMilestone, attestations))
.thenReturn(SafeFuture.completedFuture(Collections.emptyList()));

final Response response =
post(
PostAttestationsV2.ROUTE,
JsonUtil.serialize(
attestations,
SerializableTypeDefinition.listOf(
spec.atSlot(UInt64.ONE)
.getSchemaDefinitions()
.getAttestationSchema()
.castTypeToAttestationSchema()
.getJsonTypeDefinition())),
Collections.emptyMap(),
Optional.of(specMilestone.name().toLowerCase(Locale.ROOT)));

assertThat(response.code()).isEqualTo(SC_OK);
assertThat(response.body().string()).isEmpty();
}

@TestTemplate
void shouldPartiallyPostAttestations_ReturnsErrors() throws Exception {
final SubmitDataError firstSubmitDataError =
new SubmitDataError(UInt64.ZERO, "Bad attestation");
final SubmitDataError secondSubmitDataError =
new SubmitDataError(UInt64.ONE, "Very bad attestation");

final List<Attestation> attestations =
List.of(
dataStructureUtil.randomAttestation(),
dataStructureUtil.randomAttestation(),
dataStructureUtil.randomAttestation());

when(validatorApiChannel.sendSignedAttestationsV2(specMilestone, attestations))
.thenReturn(
SafeFuture.completedFuture(List.of(firstSubmitDataError, secondSubmitDataError)));

final Response response =
post(
PostAttestationsV2.ROUTE,
JsonUtil.serialize(
attestations,
SerializableTypeDefinition.listOf(
spec.atSlot(UInt64.ONE)
.getSchemaDefinitions()
.getAttestationSchema()
.castTypeToAttestationSchema()
.getJsonTypeDefinition())),
Collections.emptyMap(),
Optional.of(specMilestone.name().toLowerCase(Locale.ROOT)));

assertThat(response.code()).isEqualTo(SC_BAD_REQUEST);
final JsonNode resultAsJsonNode = JsonTestUtil.parseAsJsonNode(response.body().string());

assertThat(resultAsJsonNode.get("message").asText())
.isEqualTo("Some items failed to publish, refer to errors for details");
assertThat(resultAsJsonNode.get("failures").size()).isEqualTo(2);
assertThat(resultAsJsonNode.get("failures").get(0).get("index").asText())
.isEqualTo(firstSubmitDataError.getIndex().toString());
assertThat(resultAsJsonNode.get("failures").get(0).get("message").asText())
.isEqualTo(firstSubmitDataError.getMessage());
assertThat(resultAsJsonNode.get("failures").get(1).get("index").asText())
.isEqualTo(secondSubmitDataError.getIndex().toString());
assertThat(resultAsJsonNode.get("failures").get(1).get("message").asText())
.isEqualTo(secondSubmitDataError.getMessage());
}

@TestTemplate
void shouldFailWhenMissingConsensusHeader() throws Exception {
final List<Attestation> attestations =
List.of(dataStructureUtil.randomAttestation(), dataStructureUtil.randomAttestation());

when(validatorApiChannel.sendSignedAttestationsV2(specMilestone, attestations))
.thenReturn(SafeFuture.completedFuture(Collections.emptyList()));

final Response response =
post(
PostAttestationsV2.ROUTE,
JsonUtil.serialize(
attestations,
SerializableTypeDefinition.listOf(
spec.atSlot(UInt64.ONE)
.getSchemaDefinitions()
.getAttestationSchema()
.castTypeToAttestationSchema()
.getJsonTypeDefinition())));

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 List<Attestation> attestations =
List.of(dataStructureUtil.randomAttestation(), dataStructureUtil.randomAttestation());

when(validatorApiChannel.sendSignedAttestationsV2(specMilestone, attestations))
.thenReturn(SafeFuture.completedFuture(Collections.emptyList()));

final Response response =
post(
PostAttestationsV2.ROUTE,
JsonUtil.serialize(
attestations,
SerializableTypeDefinition.listOf(
spec.atSlot(UInt64.ONE)
.getSchemaDefinitions()
.getAttestationSchema()
.castTypeToAttestationSchema()
.getJsonTypeDefinition())),
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
@@ -0,0 +1,62 @@
{
"post" : {
"tags" : [ "Beacon", "Validator Required Api", "Experimental" ],
"operationId" : "submitPoolAttestationsV2",
"summary" : "Submit Attestation objects to node",
"description" : "Submits Attestation objects to the node. Each attestation in the request body is processed individually.\nIf an attestation is validated successfully, the node MUST publish that attestation on the appropriate subnet.\nIf one or more attestations fail validation, the node MUST return a 400 error with details of which attestations have failed, and why.",
"parameters" : [ {
"name" : "Eth-Consensus-Version",
"required" : true,
"in" : "header",
"schema" : {
"type" : "string",
"enum" : [ "phase0", "altair", "bellatrix", "capella", "deneb", "electra" ],
"description" : "Version of the attestations being submitted."
}
} ],
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "array",
"items" : {
"title" : "SignedAttestation",
"type" : "object",
"oneOf" : [ {
"$ref" : "#/components/schemas/AttestationPhase0"
}, {
"$ref" : "#/components/schemas/AttestationElectra"
} ]
}
}
}
}
},
"responses" : {
"200" : {
"description" : "Attestations are stored in pool and broadcast on appropriate subnet",
"content" : { }
},
"400" : {
"description" : "Errors with one or more attestations",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/ErrorListBadRequest"
}
}
}
},
"500" : {
"description" : "Internal server error",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/HttpErrorResponse"
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostValidatorLiveness;
import tech.pegasys.teku.beaconrestapi.handlers.v2.beacon.GetBlock;
import tech.pegasys.teku.beaconrestapi.handlers.v2.beacon.GetBlockAttestationsV2;
import tech.pegasys.teku.beaconrestapi.handlers.v2.beacon.PostAttestationsV2;
import tech.pegasys.teku.beaconrestapi.handlers.v2.beacon.PostBlindedBlockV2;
import tech.pegasys.teku.beaconrestapi.handlers.v2.beacon.PostBlockV2;
import tech.pegasys.teku.beaconrestapi.handlers.v2.debug.GetChainHeadsV2;
Expand Down Expand Up @@ -235,6 +236,7 @@ private static RestApi create(
.endpoint(new GetBlockAttestationsV2(dataProvider, schemaCache))
.endpoint(new GetAttestations(dataProvider, spec))
.endpoint(new PostAttestation(dataProvider, schemaCache))
.endpoint(new PostAttestationsV2(dataProvider, schemaCache))
.endpoint(new GetAttesterSlashings(dataProvider, spec))
.endpoint(new PostAttesterSlashing(dataProvider, spec))
.endpoint(new GetProposerSlashings(dataProvider))
Expand Down
Loading