From 917beea486f6f0885d040f052d7c225439fa8235 Mon Sep 17 00:00:00 2001 From: Lucas Saldanha Date: Tue, 27 Feb 2024 01:03:32 +1300 Subject: [PATCH] Implementing /eth/v1/validator/beacon_committee_selections (#8015) --- .../coordinator/ValidatorApiHandler.java | 7 ++ .../coordinator/ValidatorApiHandlerTest.java | 6 + ...validator_beacon_committee_selections.json | 72 +++++++++++ .../schema/BeaconCommitteeSelectionProof.json | 22 ++++ ...PostBeaconCommitteeSelectionsResponse.json | 13 ++ .../JsonTypeDefinitionBeaconRestApi.java | 3 + .../PostBeaconCommitteeSelections.java | 75 +++++++++++ .../BeaconCommitteeSelectionProof.java | 118 ++++++++++++++++++ .../validator/api/ValidatorApiChannel.java | 10 ++ .../metrics/BeaconNodeRequestLabels.java | 1 + .../MetricRecordingValidatorApiChannel.java | 9 ++ .../BeaconCommitteeSelectionsRequestTest.java | 118 ++++++++++++++++++ .../beacon_committee_selections.json | 9 ++ .../remote/FailoverValidatorApiHandler.java | 9 ++ .../remote/RemoteValidatorApiHandler.java | 7 ++ .../remote/apiclient/ValidatorApiMethod.java | 3 +- .../sentry/SentryValidatorApiChannel.java | 7 ++ .../typedef/OkHttpValidatorTypeDefClient.java | 10 ++ .../BeaconCommitteeSelectionsRequest.java | 51 ++++++++ .../remote/RemoteValidatorApiHandlerTest.java | 26 ++++ 20 files changed, 575 insertions(+), 1 deletion(-) create mode 100644 data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_validator_beacon_committee_selections.json create mode 100644 data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/BeaconCommitteeSelectionProof.json create mode 100644 data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PostBeaconCommitteeSelectionsResponse.json create mode 100644 data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostBeaconCommitteeSelections.java create mode 100644 ethereum/json-types/src/main/java/tech/pegasys/teku/ethereum/json/types/validator/BeaconCommitteeSelectionProof.java create mode 100644 validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/BeaconCommitteeSelectionsRequestTest.java create mode 100644 validator/remote/src/integration-test/resources/responses/beacon_committee_selections.json create mode 100644 validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/BeaconCommitteeSelectionsRequest.java diff --git a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandler.java b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandler.java index 5a2d617fea1..b5dce36cc52 100644 --- a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandler.java +++ b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandler.java @@ -47,6 +47,7 @@ import tech.pegasys.teku.bls.BLSPublicKey; import tech.pegasys.teku.bls.BLSSignature; import tech.pegasys.teku.ethereum.json.types.beacon.StateValidatorData; +import tech.pegasys.teku.ethereum.json.types.validator.BeaconCommitteeSelectionProof; import tech.pegasys.teku.ethereum.json.types.validator.ProposerDuties; import tech.pegasys.teku.ethereum.json.types.validator.ProposerDuty; import tech.pegasys.teku.ethereum.performance.trackers.BlockProductionPerformance; @@ -833,4 +834,10 @@ private List getProposalSlotsForEpoch(final BeaconState state, fin } return proposerSlots; } + + @Override + public SafeFuture>> getBeaconCommitteeSelectionProof( + final List requests) { + throw new UnsupportedOperationException("This method is not implemented by the Beacon Node"); + } } diff --git a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandlerTest.java b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandlerTest.java index f14df91dab9..17a9034d47d 100644 --- a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandlerTest.java +++ b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandlerTest.java @@ -1196,6 +1196,12 @@ public void checkValidatorsDoppelganger_ShouldReturnDoppelgangerDetectionResult( assertThat(validatorIsLive(validatorLivenessAtEpochsResult, thirdIndex)).isTrue(); } + @Test + public void getBeaconCommitteeSelectionProofShouldNotBeImplementedByBeaconNode() { + assertThatThrownBy(() -> validatorApiHandler.getBeaconCommitteeSelectionProof(List.of())) + .isInstanceOf(UnsupportedOperationException.class); + } + private boolean validatorIsLive( List validatorLivenessAtEpochs, UInt64 validatorIndex) { return validatorLivenessAtEpochs.stream() diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_validator_beacon_committee_selections.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_validator_beacon_committee_selections.json new file mode 100644 index 00000000000..290b2e06500 --- /dev/null +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_validator_beacon_committee_selections.json @@ -0,0 +1,72 @@ +{ + "post" : { + "tags" : [ "Validator" ], + "operationId" : "submitBeaconCommitteeSelections", + "summary" : "Determine if a distributed validator has been selected to aggregate attestations", + "description" : "This endpoint should be used by a validator client running as part of a distributed validator cluster, and is implemented by a distributed validator middleware client. This endpoint is used to exchange partial selection proofs for combined/aggregated selection proofs to allow a validator client to correctly determine if any of its validators has been selected to perform an attestation aggregation duty in a slot. Validator clients running in a distributed validator cluster must query this endpoint at the start of an epoch for the current and lookahead (next) epochs for all validators that have attester duties in the current and lookahead epochs. Consensus clients need not support this endpoint and may return a 501.", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/BeaconCommitteeSelectionProof" + } + } + } + } + }, + "responses" : { + "200" : { + "description" : "Returns the threshold aggregated beacon committee selection proofs.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PostBeaconCommitteeSelectionsResponse" + } + } + } + }, + "400" : { + "description" : "Invalid request syntax.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HttpErrorResponse" + } + } + } + }, + "500" : { + "description" : "Internal server error", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HttpErrorResponse" + } + } + } + }, + "501" : { + "description" : "Not implemented", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HttpErrorResponse" + } + } + } + }, + "503" : { + "description" : "Beacon node is currently syncing and not serving requests.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HttpErrorResponse" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/BeaconCommitteeSelectionProof.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/BeaconCommitteeSelectionProof.json new file mode 100644 index 00000000000..ca2288d9f15 --- /dev/null +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/BeaconCommitteeSelectionProof.json @@ -0,0 +1,22 @@ +{ + "title" : "BeaconCommitteeSelectionProof", + "type" : "object", + "required" : [ "validator_index", "slot", "selection_proof" ], + "properties" : { + "validator_index" : { + "type" : "string", + "description" : "integer string", + "example" : "1", + "format" : "integer" + }, + "slot" : { + "type" : "string", + "description" : "unsigned 64 bit integer", + "example" : "1", + "format" : "uint64" + }, + "selection_proof" : { + "type" : "string" + } + } +} \ No newline at end of file diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PostBeaconCommitteeSelectionsResponse.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PostBeaconCommitteeSelectionsResponse.json new file mode 100644 index 00000000000..6d280b3ee6a --- /dev/null +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PostBeaconCommitteeSelectionsResponse.json @@ -0,0 +1,13 @@ +{ + "title" : "PostBeaconCommitteeSelectionsResponse", + "type" : "object", + "required" : [ "data" ], + "properties" : { + "data" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/BeaconCommitteeSelectionProof" + } + } + } +} \ No newline at end of file diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/JsonTypeDefinitionBeaconRestApi.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/JsonTypeDefinitionBeaconRestApi.java index bb062ddc914..d3061080550 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/JsonTypeDefinitionBeaconRestApi.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/JsonTypeDefinitionBeaconRestApi.java @@ -91,6 +91,7 @@ import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.GetSyncCommitteeContribution; import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostAggregateAndProofs; import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostAttesterDuties; +import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostBeaconCommitteeSelections; import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostContributionAndProofs; import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostPrepareBeaconProposer; import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostRegisterValidator; @@ -275,6 +276,8 @@ private static RestApi create( .endpoint(new PostContributionAndProofs(dataProvider, schemaCache)) .endpoint(new PostPrepareBeaconProposer(dataProvider)) .endpoint(new PostRegisterValidator(dataProvider)) + // Obol DVT Methods + .endpoint(new PostBeaconCommitteeSelections()) // Config Handlers .endpoint( new GetDepositContract( diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostBeaconCommitteeSelections.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostBeaconCommitteeSelections.java new file mode 100644 index 00000000000..7b8f75074b5 --- /dev/null +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostBeaconCommitteeSelections.java @@ -0,0 +1,75 @@ +/* + * 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.handlers.v1.validator; + +import static tech.pegasys.teku.ethereum.json.types.validator.BeaconCommitteeSelectionProof.BEACON_COMMITTEE_SELECTION_PROOF; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NOT_IMPLEMENTED; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_VALIDATOR; +import static tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition.listOf; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import tech.pegasys.teku.ethereum.json.types.validator.BeaconCommitteeSelectionProof; +import tech.pegasys.teku.infrastructure.json.types.SerializableTypeDefinition; +import tech.pegasys.teku.infrastructure.restapi.endpoints.EndpointMetadata; +import tech.pegasys.teku.infrastructure.restapi.endpoints.RestApiEndpoint; +import tech.pegasys.teku.infrastructure.restapi.endpoints.RestApiRequest; + +public class PostBeaconCommitteeSelections extends RestApiEndpoint { + + public static final String ROUTE = "/eth/v1/validator/beacon_committee_selections"; + + private static final SerializableTypeDefinition> + RESPONSE_TYPE = + SerializableTypeDefinition.>object() + .name("PostBeaconCommitteeSelectionsResponse") + .withField("data", listOf(BEACON_COMMITTEE_SELECTION_PROOF), Function.identity()) + .build(); + + public PostBeaconCommitteeSelections() { + super( + EndpointMetadata.post(ROUTE) + .operationId("submitBeaconCommitteeSelections") + .summary( + "Determine if a distributed validator has been selected to aggregate attestations") + .description( + "This endpoint should be used by a validator client running as part of a distributed validator cluster, " + + "and is implemented by a distributed validator middleware client. This endpoint is used to " + + "exchange partial selection proofs for combined/aggregated selection proofs to allow a validator " + + "client to correctly determine if any of its validators has been selected to perform an " + + "attestation aggregation duty in a slot. Validator clients running in a distributed validator " + + "cluster must query this endpoint at the start of an epoch for the current and lookahead (next) " + + "epochs for all validators that have attester duties in the current and lookahead epochs. Consensus" + + " clients need not support this endpoint and may return a 501.") + .tags(TAG_VALIDATOR) + .requestBodyType(listOf(BEACON_COMMITTEE_SELECTION_PROOF)) + .response( + SC_OK, + "Returns the threshold aggregated beacon committee selection proofs.", + RESPONSE_TYPE) + .withBadRequestResponse(Optional.of("Invalid request syntax.")) + .withInternalErrorResponse() + .withNotImplementedResponse() + .withServiceUnavailableResponse() + .build()); + } + + @Override + public void handleRequest(final RestApiRequest request) throws JsonProcessingException { + request.respondError(SC_NOT_IMPLEMENTED, "Method not implemented by the Beacon Node"); + } +} diff --git a/ethereum/json-types/src/main/java/tech/pegasys/teku/ethereum/json/types/validator/BeaconCommitteeSelectionProof.java b/ethereum/json-types/src/main/java/tech/pegasys/teku/ethereum/json/types/validator/BeaconCommitteeSelectionProof.java new file mode 100644 index 00000000000..555a37220be --- /dev/null +++ b/ethereum/json-types/src/main/java/tech/pegasys/teku/ethereum/json/types/validator/BeaconCommitteeSelectionProof.java @@ -0,0 +1,118 @@ +/* + * 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.ethereum.json.types.validator; + +import java.util.Objects; +import tech.pegasys.teku.infrastructure.json.types.CoreTypes; +import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; + +public class BeaconCommitteeSelectionProof { + + public static final DeserializableTypeDefinition + BEACON_COMMITTEE_SELECTION_PROOF = + DeserializableTypeDefinition.object( + BeaconCommitteeSelectionProof.class, BeaconCommitteeSelectionProof.Builder.class) + .name("BeaconCommitteeSelectionProof") + .initializer(BeaconCommitteeSelectionProof::builder) + .finisher(BeaconCommitteeSelectionProof.Builder::build) + .withField( + "validator_index", + CoreTypes.INTEGER_TYPE, + BeaconCommitteeSelectionProof::getValidatorIndex, + BeaconCommitteeSelectionProof.Builder::validatorIndex) + .withField( + "slot", + CoreTypes.UINT64_TYPE, + BeaconCommitteeSelectionProof::getSlot, + BeaconCommitteeSelectionProof.Builder::slot) + .withField( + "selection_proof", + CoreTypes.STRING_TYPE, + BeaconCommitteeSelectionProof::getSelectionProof, + BeaconCommitteeSelectionProof.Builder::selectionProof) + .build(); + + private final int validatorIndex; + private final UInt64 slot; + private final String selectionProof; + + private BeaconCommitteeSelectionProof( + final int validatorIndex, final UInt64 slot, final String selectionProof) { + this.validatorIndex = validatorIndex; + this.slot = slot; + this.selectionProof = selectionProof; + } + + public int getValidatorIndex() { + return validatorIndex; + } + + public UInt64 getSlot() { + return slot; + } + + public String getSelectionProof() { + return selectionProof; + } + + public static BeaconCommitteeSelectionProof.Builder builder() { + return new BeaconCommitteeSelectionProof.Builder(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final BeaconCommitteeSelectionProof that = (BeaconCommitteeSelectionProof) o; + return validatorIndex == that.validatorIndex + && Objects.equals(slot, that.slot) + && Objects.equals(selectionProof, that.selectionProof); + } + + @Override + public int hashCode() { + return Objects.hash(validatorIndex, slot, selectionProof); + } + + public static class Builder { + + private int validatorIndex; + private UInt64 slot; + private String selectionProof; + + public Builder validatorIndex(final int validatorIndex) { + this.validatorIndex = validatorIndex; + return this; + } + + public Builder slot(final UInt64 slot) { + this.slot = slot; + return this; + } + + public Builder selectionProof(final String selectionProof) { + this.selectionProof = selectionProof; + return this; + } + + public BeaconCommitteeSelectionProof build() { + return new BeaconCommitteeSelectionProof(validatorIndex, slot, selectionProof); + } + } +} diff --git a/validator/api/src/main/java/tech/pegasys/teku/validator/api/ValidatorApiChannel.java b/validator/api/src/main/java/tech/pegasys/teku/validator/api/ValidatorApiChannel.java index bab3f259560..d446595a44c 100644 --- a/validator/api/src/main/java/tech/pegasys/teku/validator/api/ValidatorApiChannel.java +++ b/validator/api/src/main/java/tech/pegasys/teku/validator/api/ValidatorApiChannel.java @@ -24,6 +24,7 @@ import tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus; import tech.pegasys.teku.bls.BLSPublicKey; import tech.pegasys.teku.bls.BLSSignature; +import tech.pegasys.teku.ethereum.json.types.validator.BeaconCommitteeSelectionProof; import tech.pegasys.teku.ethereum.json.types.validator.ProposerDuties; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.events.ChannelInterface; @@ -174,6 +175,12 @@ public SafeFuture>> getValidatorsLivenes List validatorIndices, UInt64 epoch) { return SafeFuture.completedFuture(Optional.empty()); } + + @Override + public SafeFuture>> + getBeaconCommitteeSelectionProof(final List requests) { + return SafeFuture.completedFuture(Optional.of(requests)); + } }; int UNKNOWN_VALIDATOR_ID = -1; @@ -250,4 +257,7 @@ SafeFuture prepareBeaconProposer( SafeFuture>> getValidatorsLiveness( List validatorIndices, UInt64 epoch); + + SafeFuture>> getBeaconCommitteeSelectionProof( + List requests); } diff --git a/validator/beaconnode/src/main/java/tech/pegasys/teku/validator/beaconnode/metrics/BeaconNodeRequestLabels.java b/validator/beaconnode/src/main/java/tech/pegasys/teku/validator/beaconnode/metrics/BeaconNodeRequestLabels.java index fccb542e4df..d05943ee77c 100644 --- a/validator/beaconnode/src/main/java/tech/pegasys/teku/validator/beaconnode/metrics/BeaconNodeRequestLabels.java +++ b/validator/beaconnode/src/main/java/tech/pegasys/teku/validator/beaconnode/metrics/BeaconNodeRequestLabels.java @@ -39,4 +39,5 @@ public class BeaconNodeRequestLabels { public static final String PREPARE_BEACON_PROPOSERS_METHOD = "prepare_beacon_proposers"; public static final String REGISTER_VALIDATORS_METHOD = "register_validators"; public static final String GET_VALIDATORS_LIVENESS = "get_validators_liveness"; + public static final String BEACON_COMMITTEE_SELECTIONS = "beacon_committee_selections"; } diff --git a/validator/beaconnode/src/main/java/tech/pegasys/teku/validator/beaconnode/metrics/MetricRecordingValidatorApiChannel.java b/validator/beaconnode/src/main/java/tech/pegasys/teku/validator/beaconnode/metrics/MetricRecordingValidatorApiChannel.java index 0a62489acb6..ef1242c5936 100644 --- a/validator/beaconnode/src/main/java/tech/pegasys/teku/validator/beaconnode/metrics/MetricRecordingValidatorApiChannel.java +++ b/validator/beaconnode/src/main/java/tech/pegasys/teku/validator/beaconnode/metrics/MetricRecordingValidatorApiChannel.java @@ -32,6 +32,7 @@ import tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus; import tech.pegasys.teku.bls.BLSPublicKey; import tech.pegasys.teku.bls.BLSSignature; +import tech.pegasys.teku.ethereum.json.types.validator.BeaconCommitteeSelectionProof; import tech.pegasys.teku.ethereum.json.types.validator.ProposerDuties; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; @@ -254,6 +255,14 @@ public SafeFuture>> getValidatorsLivenes BeaconNodeRequestLabels.GET_VALIDATORS_LIVENESS); } + @Override + public SafeFuture>> getBeaconCommitteeSelectionProof( + final List requests) { + return countDataRequest( + delegate.getBeaconCommitteeSelectionProof(requests), + BeaconNodeRequestLabels.BEACON_COMMITTEE_SELECTIONS); + } + private SafeFuture countDataRequest( final SafeFuture request, final String requestName) { return request diff --git a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/BeaconCommitteeSelectionsRequestTest.java b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/BeaconCommitteeSelectionsRequestTest.java new file mode 100644 index 00000000000..e895a6483cd --- /dev/null +++ b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/BeaconCommitteeSelectionsRequestTest.java @@ -0,0 +1,118 @@ +/* + * Copyright Consensys Software Inc., 2023 + * + * 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.validator.remote.typedef.handlers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NOT_IMPLEMENTED; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_SERVICE_UNAVAILABLE; +import static tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition.listOf; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import tech.pegasys.teku.ethereum.json.types.validator.BeaconCommitteeSelectionProof; +import tech.pegasys.teku.infrastructure.json.JsonUtil; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.TestSpecContext; +import tech.pegasys.teku.spec.networks.Eth2Network; +import tech.pegasys.teku.validator.remote.apiclient.ValidatorApiMethod; +import tech.pegasys.teku.validator.remote.typedef.AbstractTypeDefRequestTestBase; + +@TestSpecContext(network = Eth2Network.MINIMAL) +public class BeaconCommitteeSelectionsRequestTest extends AbstractTypeDefRequestTestBase { + + private BeaconCommitteeSelectionsRequest request; + private List entries; + + @BeforeEach + void setupRequest() { + request = new BeaconCommitteeSelectionsRequest(mockWebServer.url("/"), okHttpClient); + entries = List.of(createBeaconCommitteeSelectionProof()); + } + + @TestTemplate + public void correctResponseDeserialization() { + final String mockResponse = readResource("responses/beacon_committee_selections.json"); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_OK).setBody(mockResponse)); + + // values from beacon_committee_selections.json + final BeaconCommitteeSelectionProof expectedBeaconCommitteeSelectionProof = + new BeaconCommitteeSelectionProof.Builder() + .validatorIndex(1) + .slot(UInt64.ONE) + .selectionProof( + "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505") + .build(); + + final Optional> response = + request.getSelectionProof(entries); + assertThat(response).isPresent().contains(List.of(expectedBeaconCommitteeSelectionProof)); + } + + @TestTemplate + public void expectedRequest() throws Exception { + final String mockResponse = readResource("responses/beacon_committee_selections.json"); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_OK).setBody(mockResponse)); + + request.getSelectionProof(entries); + + final RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()) + .contains(ValidatorApiMethod.BEACON_COMMITTEE_SELECTIONS.getPath(Collections.emptyMap())); + + final List entriesSent = + JsonUtil.parse( + request.getBody().readUtf8(), + listOf(BeaconCommitteeSelectionProof.BEACON_COMMITTEE_SELECTION_PROOF)); + assertThat(entriesSent).isEqualTo(entries); + } + + @TestTemplate + public void handlingBadRequest() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_BAD_REQUEST)); + + assertThatThrownBy(() -> request.getSelectionProof(entries)) + .isInstanceOf(IllegalArgumentException.class); + } + + @TestTemplate + public void handlingNotImplemented() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NOT_IMPLEMENTED)); + + assertThat(request.getSelectionProof(entries)).isEmpty(); + } + + @TestTemplate + public void handlingSyncing() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_SERVICE_UNAVAILABLE)); + + assertThat(request.getSelectionProof(entries)).isEmpty(); + } + + private BeaconCommitteeSelectionProof createBeaconCommitteeSelectionProof() { + return new BeaconCommitteeSelectionProof.Builder() + .validatorIndex(dataStructureUtil.randomPositiveInt()) + .slot(dataStructureUtil.randomUInt64()) + .selectionProof(dataStructureUtil.randomSignature().toBytesCompressed().toHexString()) + .build(); + } +} diff --git a/validator/remote/src/integration-test/resources/responses/beacon_committee_selections.json b/validator/remote/src/integration-test/resources/responses/beacon_committee_selections.json new file mode 100644 index 00000000000..14c5a2d6649 --- /dev/null +++ b/validator/remote/src/integration-test/resources/responses/beacon_committee_selections.json @@ -0,0 +1,9 @@ +{ + "data": [ + { + "validator_index": "1", + "slot": "1", + "selection_proof": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + } + ] +} \ No newline at end of file diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/FailoverValidatorApiHandler.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/FailoverValidatorApiHandler.java index ebd7fe31075..6383aaf273e 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/FailoverValidatorApiHandler.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/FailoverValidatorApiHandler.java @@ -33,6 +33,7 @@ import tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus; import tech.pegasys.teku.bls.BLSPublicKey; import tech.pegasys.teku.bls.BLSSignature; +import tech.pegasys.teku.ethereum.json.types.validator.BeaconCommitteeSelectionProof; import tech.pegasys.teku.ethereum.json.types.validator.ProposerDuties; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.collections.LimitedMap; @@ -299,6 +300,14 @@ public SafeFuture>> getValidatorsLivenes BeaconNodeRequestLabels.GET_VALIDATORS_LIVENESS); } + @Override + public SafeFuture>> getBeaconCommitteeSelectionProof( + final List request) { + return relayRequest( + apiChannel -> apiChannel.getBeaconCommitteeSelectionProof(request), + BeaconNodeRequestLabels.BEACON_COMMITTEE_SELECTIONS); + } + private SafeFuture relayRequest( final ValidatorApiChannelRequest request, final String method) { return relayRequest(request, method, true); diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/RemoteValidatorApiHandler.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/RemoteValidatorApiHandler.java index 5fb4f3936a5..e556ba7a790 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/RemoteValidatorApiHandler.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/RemoteValidatorApiHandler.java @@ -46,6 +46,7 @@ import tech.pegasys.teku.bls.BLSPublicKey; import tech.pegasys.teku.bls.BLSSignature; import tech.pegasys.teku.ethereum.json.types.beacon.StateValidatorData; +import tech.pegasys.teku.ethereum.json.types.validator.BeaconCommitteeSelectionProof; import tech.pegasys.teku.ethereum.json.types.validator.ProposerDuties; import tech.pegasys.teku.infrastructure.async.AsyncRunner; import tech.pegasys.teku.infrastructure.async.ExceptionThrowingRunnable; @@ -487,6 +488,12 @@ public SafeFuture>> getValidatorsLivenes .map(this::responseToValidatorsLivenessResult)); } + @Override + public SafeFuture>> getBeaconCommitteeSelectionProof( + final List request) { + return sendRequest(() -> typeDefClient.getBeaconCommitteeSelectionProof(request)); + } + private List responseToValidatorsLivenessResult( final PostValidatorLivenessResponse response) { return response.data.stream() diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/apiclient/ValidatorApiMethod.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/apiclient/ValidatorApiMethod.java index 650277d0d12..8229e9af00a 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/apiclient/ValidatorApiMethod.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/apiclient/ValidatorApiMethod.java @@ -47,7 +47,8 @@ public enum ValidatorApiMethod { GET_BLOCK_HEADER("eth/v1/beacon/headers/:block_id"), GET_CONFIG_SPEC("/eth/v1/config/spec"), EVENTS("eth/v1/events"), - SEND_VALIDATOR_LIVENESS("/eth/v1/validator/liveness/:epoch"); + SEND_VALIDATOR_LIVENESS("/eth/v1/validator/liveness/:epoch"), + BEACON_COMMITTEE_SELECTIONS("/eth/v1/validator/beacon_committee_selections"); private final String path; diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/sentry/SentryValidatorApiChannel.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/sentry/SentryValidatorApiChannel.java index a1c41f3ed6b..147206afc00 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/sentry/SentryValidatorApiChannel.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/sentry/SentryValidatorApiChannel.java @@ -24,6 +24,7 @@ import tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus; import tech.pegasys.teku.bls.BLSPublicKey; import tech.pegasys.teku.bls.BLSSignature; +import tech.pegasys.teku.ethereum.json.types.validator.BeaconCommitteeSelectionProof; import tech.pegasys.teku.ethereum.json.types.validator.ProposerDuties; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.ssz.SszList; @@ -219,4 +220,10 @@ public SafeFuture>> getValidatorsLivenes List validatorIndices, UInt64 epoch) { return dutiesProviderChannel.getValidatorsLiveness(validatorIndices, epoch); } + + @Override + public SafeFuture>> getBeaconCommitteeSelectionProof( + final List request) { + return dutiesProviderChannel.getBeaconCommitteeSelectionProof(request); + } } diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/OkHttpValidatorTypeDefClient.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/OkHttpValidatorTypeDefClient.java index 2ae727f2e03..241548c49fa 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/OkHttpValidatorTypeDefClient.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/OkHttpValidatorTypeDefClient.java @@ -22,6 +22,7 @@ import org.apache.tuweni.bytes.Bytes32; import tech.pegasys.teku.bls.BLSSignature; import tech.pegasys.teku.ethereum.json.types.beacon.StateValidatorData; +import tech.pegasys.teku.ethereum.json.types.validator.BeaconCommitteeSelectionProof; import tech.pegasys.teku.ethereum.json.types.validator.ProposerDuties; import tech.pegasys.teku.infrastructure.ssz.SszList; import tech.pegasys.teku.infrastructure.unsigned.UInt64; @@ -34,6 +35,7 @@ import tech.pegasys.teku.spec.datastructures.operations.AttestationData; import tech.pegasys.teku.validator.api.SendSignedBlockResult; import tech.pegasys.teku.validator.api.required.SyncingStatus; +import tech.pegasys.teku.validator.remote.typedef.handlers.BeaconCommitteeSelectionsRequest; import tech.pegasys.teku.validator.remote.typedef.handlers.CreateAttestationDataRequest; import tech.pegasys.teku.validator.remote.typedef.handlers.CreateBlockRequest; import tech.pegasys.teku.validator.remote.typedef.handlers.GetGenesisRequest; @@ -57,6 +59,7 @@ public class OkHttpValidatorTypeDefClient extends OkHttpValidatorMinimalTypeDefC private final SendSignedBlockRequest sendSignedBlockRequest; private final RegisterValidatorsRequest registerValidatorsRequest; private final CreateAttestationDataRequest createAttestationDataRequest; + private final BeaconCommitteeSelectionsRequest beaconCommitteeSelectionsRequest; public OkHttpValidatorTypeDefClient( final OkHttpClient okHttpClient, @@ -76,6 +79,8 @@ public OkHttpValidatorTypeDefClient( new RegisterValidatorsRequest(baseEndpoint, okHttpClient, false); this.createAttestationDataRequest = new CreateAttestationDataRequest(baseEndpoint, okHttpClient); + this.beaconCommitteeSelectionsRequest = + new BeaconCommitteeSelectionsRequest(baseEndpoint, okHttpClient); } public SyncingStatus getSyncingStatus() { @@ -152,4 +157,9 @@ public Optional createAttestationData( final UInt64 slot, final int committeeIndex) { return createAttestationDataRequest.createAttestationData(slot, committeeIndex); } + + public Optional> getBeaconCommitteeSelectionProof( + final List validatorsPartialProofs) { + return beaconCommitteeSelectionsRequest.getSelectionProof(validatorsPartialProofs); + } } diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/BeaconCommitteeSelectionsRequest.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/BeaconCommitteeSelectionsRequest.java new file mode 100644 index 00000000000..02c64f85c37 --- /dev/null +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/BeaconCommitteeSelectionsRequest.java @@ -0,0 +1,51 @@ +/* + * 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.validator.remote.typedef.handlers; + +import static tech.pegasys.teku.ethereum.json.types.validator.BeaconCommitteeSelectionProof.BEACON_COMMITTEE_SELECTION_PROOF; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NOT_IMPLEMENTED; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_SERVICE_UNAVAILABLE; +import static tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition.listOf; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import tech.pegasys.teku.ethereum.json.types.SharedApiTypes; +import tech.pegasys.teku.ethereum.json.types.validator.BeaconCommitteeSelectionProof; +import tech.pegasys.teku.validator.remote.apiclient.ValidatorApiMethod; +import tech.pegasys.teku.validator.remote.typedef.ResponseHandler; + +public class BeaconCommitteeSelectionsRequest extends AbstractTypeDefRequest { + + public BeaconCommitteeSelectionsRequest( + final HttpUrl baseEndpoint, final OkHttpClient okHttpClient) { + super(baseEndpoint, okHttpClient); + } + + public Optional> getSelectionProof( + final List validatorsPartialProof) { + return postJson( + ValidatorApiMethod.BEACON_COMMITTEE_SELECTIONS, + Collections.emptyMap(), + validatorsPartialProof, + listOf(BEACON_COMMITTEE_SELECTION_PROOF), + new ResponseHandler<>( + SharedApiTypes.withDataWrapper( + "BeaconCommitteeSelectionsResponse", listOf(BEACON_COMMITTEE_SELECTION_PROOF))) + .withHandler(SC_NOT_IMPLEMENTED, (request, response) -> Optional.empty()) + .withHandler(SC_SERVICE_UNAVAILABLE, (request, response) -> Optional.empty())); + } +} diff --git a/validator/remote/src/test/java/tech/pegasys/teku/validator/remote/RemoteValidatorApiHandlerTest.java b/validator/remote/src/test/java/tech/pegasys/teku/validator/remote/RemoteValidatorApiHandlerTest.java index 1c25804ad7c..4814c65b76c 100644 --- a/validator/remote/src/test/java/tech/pegasys/teku/validator/remote/RemoteValidatorApiHandlerTest.java +++ b/validator/remote/src/test/java/tech/pegasys/teku/validator/remote/RemoteValidatorApiHandlerTest.java @@ -60,6 +60,7 @@ import tech.pegasys.teku.bls.BLSPublicKey; import tech.pegasys.teku.bls.BLSSignature; import tech.pegasys.teku.ethereum.json.types.beacon.StateValidatorData; +import tech.pegasys.teku.ethereum.json.types.validator.BeaconCommitteeSelectionProof; import tech.pegasys.teku.ethereum.json.types.validator.ProposerDuties; import tech.pegasys.teku.ethereum.json.types.validator.ProposerDuty; import tech.pegasys.teku.infrastructure.async.SafeFuture; @@ -112,6 +113,31 @@ public void beforeEach() { new RemoteValidatorApiHandler(endpoint, spec, apiClient, typeDefClient, asyncRunner, true); } + @Test + public void beaconCommitteeSelectionsRequest_ReturnBeaconCommitteeSelectionProof() { + final String blsSignatureHex = + dataStructureUtil.randomSignature().toBytesCompressed().toHexString(); + final BeaconCommitteeSelectionProof proof = + new BeaconCommitteeSelectionProof.Builder() + .validatorIndex(1) + .slot(ONE) + .selectionProof(blsSignatureHex) + .build(); + + when(typeDefClient.getBeaconCommitteeSelectionProof(any())) + .thenReturn(Optional.of(List.of(proof))); + + final SafeFuture>> future = + apiHandler.getBeaconCommitteeSelectionProof(List.of(proof)); + asyncRunner.executeQueuedActions(); + + final List response = unwrapToValue(future); + final BeaconCommitteeSelectionProof responseProof = response.get(0); + assertThat(responseProof.getValidatorIndex()).isEqualTo(proof.getValidatorIndex()); + assertThat(responseProof.getSlot()).isEqualTo(proof.getSlot()); + assertThat(responseProof.getSelectionProof()).isEqualTo(proof.getSelectionProof()); + } + @Test public void getsEndpoint() { assertThat(apiHandler.getEndpoint()).isEqualTo(endpoint);