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 b5dce36cc52..2417ef0a25c 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 @@ -50,6 +50,7 @@ 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.json.types.validator.SyncCommitteeSelectionProof; import tech.pegasys.teku.ethereum.performance.trackers.BlockProductionPerformance; import tech.pegasys.teku.ethereum.performance.trackers.BlockProductionPerformanceFactory; import tech.pegasys.teku.infrastructure.async.SafeFuture; @@ -840,4 +841,10 @@ public SafeFuture>> getBeaconCommit final List requests) { throw new UnsupportedOperationException("This method is not implemented by the Beacon Node"); } + + @Override + public SafeFuture>> getSyncCommitteeSelectionProof( + 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 17a9034d47d..a2d19eb855d 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 @@ -1202,6 +1202,12 @@ public void getBeaconCommitteeSelectionProofShouldNotBeImplementedByBeaconNode() .isInstanceOf(UnsupportedOperationException.class); } + @Test + public void getSyncCommitteeSelectionProofShouldNotBeImplementedByBeaconNode() { + assertThatThrownBy(() -> validatorApiHandler.getSyncCommitteeSelectionProof(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_sync_committee_selections.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_validator_sync_committee_selections.json new file mode 100644 index 00000000000..953ded663e2 --- /dev/null +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_validator_sync_committee_selections.json @@ -0,0 +1,72 @@ +{ + "post" : { + "tags" : [ "Validator" ], + "operationId" : "submitSyncCommitteeSelections", + "summary" : "Determine if a distributed validator has been selected to make a sync committee contribution", + "description" : "Submit sync committee selections to a DVT middleware client. It returns the threshold aggregated sync committee selection. 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 proof slot signatures for combined/aggregated selection proofs to allow a validator client to correctly determine if one of its validators has been selected to perform a sync committee contribution (sync aggregation) duty in this slot. Consensus clients need not support this endpoint and may return a 501.", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/SyncCommitteeSelectionProof" + } + } + } + } + }, + "responses" : { + "200" : { + "description" : "Returns the threshold aggregated sync committee selection proofs.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PostSyncCommitteeSelectionsResponse" + } + } + } + }, + "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/PostSyncCommitteeSelectionsResponse.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PostSyncCommitteeSelectionsResponse.json new file mode 100644 index 00000000000..d4364dd5d90 --- /dev/null +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PostSyncCommitteeSelectionsResponse.json @@ -0,0 +1,13 @@ +{ + "title" : "PostSyncCommitteeSelectionsResponse", + "type" : "object", + "required" : [ "data" ], + "properties" : { + "data" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/SyncCommitteeSelectionProof" + } + } + } +} \ No newline at end of file diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/SyncCommitteeSelectionProof.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/SyncCommitteeSelectionProof.json new file mode 100644 index 00000000000..68132ae2e21 --- /dev/null +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/SyncCommitteeSelectionProof.json @@ -0,0 +1,28 @@ +{ + "title" : "SyncCommitteeSelectionProof", + "type" : "object", + "required" : [ "validator_index", "slot", "subcommittee_index", "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" + }, + "subcommittee_index" : { + "type" : "string", + "description" : "integer string", + "example" : "1", + "format" : "integer" + }, + "selection_proof" : { + "type" : "string" + } + } +} \ 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 d3061080550..480ee9fec01 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 @@ -96,6 +96,7 @@ import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostPrepareBeaconProposer; import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostRegisterValidator; import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostSubscribeToBeaconCommitteeSubnet; +import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostSyncCommitteeSelections; import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostSyncCommitteeSubscriptions; import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostSyncDuties; import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostValidatorLiveness; @@ -278,6 +279,7 @@ private static RestApi create( .endpoint(new PostRegisterValidator(dataProvider)) // Obol DVT Methods .endpoint(new PostBeaconCommitteeSelections()) + .endpoint(new PostSyncCommitteeSelections()) // Config Handlers .endpoint( new GetDepositContract( diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostSyncCommitteeSelections.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostSyncCommitteeSelections.java new file mode 100644 index 00000000000..f8074bf6394 --- /dev/null +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostSyncCommitteeSelections.java @@ -0,0 +1,74 @@ +/* + * 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.SyncCommitteeSelectionProof.SYNC_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.SyncCommitteeSelectionProof; +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 PostSyncCommitteeSelections extends RestApiEndpoint { + + public static final String ROUTE = "/eth/v1/validator/sync_committee_selections"; + + private static final SerializableTypeDefinition> RESPONSE_TYPE = + SerializableTypeDefinition.>object() + .name("PostSyncCommitteeSelectionsResponse") + .withField("data", listOf(SYNC_COMMITTEE_SELECTION_PROOF), Function.identity()) + .build(); + + public PostSyncCommitteeSelections() { + super( + EndpointMetadata.post(ROUTE) + .operationId("submitSyncCommitteeSelections") + .summary( + "Determine if a distributed validator has been selected to make a sync committee contribution") + .description( + "Submit sync committee selections to a DVT middleware client. It returns the threshold aggregated " + + "sync committee selection. 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 proof slot signatures for " + + "combined/aggregated selection proofs to allow a validator client to correctly determine if one" + + " of its validators has been selected to perform a sync committee contribution (sync " + + "aggregation) duty in this slot. Consensus clients need not support this endpoint and may " + + "return a 501.") + .tags(TAG_VALIDATOR) + .requestBodyType(listOf(SYNC_COMMITTEE_SELECTION_PROOF)) + .response( + SC_OK, + "Returns the threshold aggregated sync 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/SyncCommitteeSelectionProof.java b/ethereum/json-types/src/main/java/tech/pegasys/teku/ethereum/json/types/validator/SyncCommitteeSelectionProof.java new file mode 100644 index 00000000000..83144ddfb02 --- /dev/null +++ b/ethereum/json-types/src/main/java/tech/pegasys/teku/ethereum/json/types/validator/SyncCommitteeSelectionProof.java @@ -0,0 +1,140 @@ +/* + * 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 SyncCommitteeSelectionProof { + + public static final DeserializableTypeDefinition + SYNC_COMMITTEE_SELECTION_PROOF = + DeserializableTypeDefinition.object( + SyncCommitteeSelectionProof.class, SyncCommitteeSelectionProof.Builder.class) + .name("SyncCommitteeSelectionProof") + .initializer(SyncCommitteeSelectionProof::builder) + .finisher(SyncCommitteeSelectionProof.Builder::build) + .withField( + "validator_index", + CoreTypes.INTEGER_TYPE, + SyncCommitteeSelectionProof::getValidatorIndex, + SyncCommitteeSelectionProof.Builder::validatorIndex) + .withField( + "slot", + CoreTypes.UINT64_TYPE, + SyncCommitteeSelectionProof::getSlot, + SyncCommitteeSelectionProof.Builder::slot) + .withField( + "subcommittee_index", + CoreTypes.INTEGER_TYPE, + SyncCommitteeSelectionProof::getSubcommitteeIndex, + SyncCommitteeSelectionProof.Builder::subcommitteeIndex) + .withField( + "selection_proof", + CoreTypes.STRING_TYPE, + SyncCommitteeSelectionProof::getSelectionProof, + SyncCommitteeSelectionProof.Builder::selectionProof) + .build(); + + private final int validatorIndex; + private final UInt64 slot; + private final int subcommitteeIndex; + private final String selectionProof; + + private SyncCommitteeSelectionProof( + final int validatorIndex, + final UInt64 slot, + final int subcommitteeIndex, + final String selectionProof) { + this.validatorIndex = validatorIndex; + this.slot = slot; + this.subcommitteeIndex = subcommitteeIndex; + this.selectionProof = selectionProof; + } + + public int getValidatorIndex() { + return validatorIndex; + } + + public UInt64 getSlot() { + return slot; + } + + public int getSubcommitteeIndex() { + return subcommitteeIndex; + } + + public String getSelectionProof() { + return selectionProof; + } + + public static SyncCommitteeSelectionProof.Builder builder() { + return new SyncCommitteeSelectionProof.Builder(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final SyncCommitteeSelectionProof that = (SyncCommitteeSelectionProof) o; + return validatorIndex == that.validatorIndex + && subcommitteeIndex == that.subcommitteeIndex + && Objects.equals(slot, that.slot) + && Objects.equals(selectionProof, that.selectionProof); + } + + @Override + public int hashCode() { + return Objects.hash(validatorIndex, slot, subcommitteeIndex, selectionProof); + } + + public static class Builder { + + private int validatorIndex; + private UInt64 slot; + private int subcommitteeIndex; + private String selectionProof; + + public Builder validatorIndex(final int validatorIndex) { + this.validatorIndex = validatorIndex; + return this; + } + + public Builder subcommitteeIndex(final int subcommitteeIndex) { + this.subcommitteeIndex = subcommitteeIndex; + 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 SyncCommitteeSelectionProof build() { + return new SyncCommitteeSelectionProof( + validatorIndex, slot, subcommitteeIndex, 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 d446595a44c..11d9a8568f5 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 @@ -26,6 +26,7 @@ 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.ethereum.json.types.validator.SyncCommitteeSelectionProof; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.events.ChannelInterface; import tech.pegasys.teku.infrastructure.ssz.SszList; @@ -181,6 +182,12 @@ public SafeFuture>> getValidatorsLivenes getBeaconCommitteeSelectionProof(final List requests) { return SafeFuture.completedFuture(Optional.of(requests)); } + + @Override + public SafeFuture>> + getSyncCommitteeSelectionProof(final List requests) { + return SafeFuture.completedFuture(Optional.of(requests)); + } }; int UNKNOWN_VALIDATOR_ID = -1; @@ -260,4 +267,7 @@ SafeFuture>> getValidatorsLiveness( SafeFuture>> getBeaconCommitteeSelectionProof( List requests); + + SafeFuture>> getSyncCommitteeSelectionProof( + 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 d05943ee77c..2e86794a95e 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 @@ -40,4 +40,5 @@ public class BeaconNodeRequestLabels { 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"; + public static final String SYNC_COMMITTEE_SELECTIONS = "sync_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 ef1242c5936..798cfad3d51 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 @@ -34,6 +34,7 @@ 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.ethereum.json.types.validator.SyncCommitteeSelectionProof; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; import tech.pegasys.teku.infrastructure.metrics.Validator.ValidatorDutyMetricUtils; @@ -263,6 +264,14 @@ public SafeFuture>> getBeaconCommit BeaconNodeRequestLabels.BEACON_COMMITTEE_SELECTIONS); } + @Override + public SafeFuture>> getSyncCommitteeSelectionProof( + final List requests) { + return countDataRequest( + delegate.getSyncCommitteeSelectionProof(requests), + BeaconNodeRequestLabels.SYNC_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/SyncCommitteeSelectionsRequestTest.java b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SyncCommitteeSelectionsRequestTest.java new file mode 100644 index 00000000000..294deded7fd --- /dev/null +++ b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SyncCommitteeSelectionsRequestTest.java @@ -0,0 +1,119 @@ +/* + * 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.SyncCommitteeSelectionProof; +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 SyncCommitteeSelectionsRequestTest extends AbstractTypeDefRequestTestBase { + + private SyncCommitteeSelectionsRequest request; + private List entries; + + @BeforeEach + void setupRequest() { + request = new SyncCommitteeSelectionsRequest(mockWebServer.url("/"), okHttpClient); + entries = List.of(createSyncCommitteeSelectionProof()); + } + + @TestTemplate + public void correctResponseDeserialization() { + final String mockResponse = readResource("responses/sync_committee_selections.json"); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_OK).setBody(mockResponse)); + + // values from sync_committee_selections.json + final SyncCommitteeSelectionProof expectedSyncCommitteeSelectionProof = + new SyncCommitteeSelectionProof.Builder() + .validatorIndex(1) + .slot(UInt64.ONE) + .subcommitteeIndex(1) + .selectionProof( + "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505") + .build(); + + final Optional> response = request.getSelectionProof(entries); + assertThat(response).isPresent().contains(List.of(expectedSyncCommitteeSelectionProof)); + } + + @TestTemplate + public void expectedRequest() throws Exception { + final String mockResponse = readResource("responses/sync_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.SYNC_COMMITTEE_SELECTIONS.getPath(Collections.emptyMap())); + + final List entriesSent = + JsonUtil.parse( + request.getBody().readUtf8(), + listOf(SyncCommitteeSelectionProof.SYNC_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 SyncCommitteeSelectionProof createSyncCommitteeSelectionProof() { + return new SyncCommitteeSelectionProof.Builder() + .validatorIndex(dataStructureUtil.randomPositiveInt()) + .slot(dataStructureUtil.randomUInt64()) + .subcommitteeIndex(dataStructureUtil.randomPositiveInt()) + .selectionProof(dataStructureUtil.randomSignature().toBytesCompressed().toHexString()) + .build(); + } +} diff --git a/validator/remote/src/integration-test/resources/responses/sync_committee_selections.json b/validator/remote/src/integration-test/resources/responses/sync_committee_selections.json new file mode 100644 index 00000000000..6b6c21d6e8b --- /dev/null +++ b/validator/remote/src/integration-test/resources/responses/sync_committee_selections.json @@ -0,0 +1,10 @@ +{ + "data": [ + { + "validator_index": "1", + "slot": "1", + "subcommittee_index": "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 6383aaf273e..6dc98a58d72 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 @@ -35,6 +35,7 @@ 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.ethereum.json.types.validator.SyncCommitteeSelectionProof; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.collections.LimitedMap; import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; @@ -302,12 +303,20 @@ public SafeFuture>> getValidatorsLivenes @Override public SafeFuture>> getBeaconCommitteeSelectionProof( - final List request) { + final List requests) { return relayRequest( - apiChannel -> apiChannel.getBeaconCommitteeSelectionProof(request), + apiChannel -> apiChannel.getBeaconCommitteeSelectionProof(requests), BeaconNodeRequestLabels.BEACON_COMMITTEE_SELECTIONS); } + @Override + public SafeFuture>> getSyncCommitteeSelectionProof( + final List requests) { + return relayRequest( + apiChannel -> apiChannel.getSyncCommitteeSelectionProof(requests), + BeaconNodeRequestLabels.SYNC_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 e556ba7a790..3fa2c0a0e21 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 @@ -48,6 +48,7 @@ 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.SyncCommitteeSelectionProof; import tech.pegasys.teku.infrastructure.async.AsyncRunner; import tech.pegasys.teku.infrastructure.async.ExceptionThrowingRunnable; import tech.pegasys.teku.infrastructure.async.ExceptionThrowingSupplier; @@ -490,8 +491,14 @@ public SafeFuture>> getValidatorsLivenes @Override public SafeFuture>> getBeaconCommitteeSelectionProof( - final List request) { - return sendRequest(() -> typeDefClient.getBeaconCommitteeSelectionProof(request)); + final List requests) { + return sendRequest(() -> typeDefClient.getBeaconCommitteeSelectionProof(requests)); + } + + @Override + public SafeFuture>> getSyncCommitteeSelectionProof( + final List requests) { + return sendRequest(() -> typeDefClient.getSyncCommitteeSelectionProof(requests)); } private List responseToValidatorsLivenessResult( 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 8229e9af00a..e7c8dd8c0b6 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 @@ -48,7 +48,8 @@ public enum ValidatorApiMethod { GET_CONFIG_SPEC("/eth/v1/config/spec"), EVENTS("eth/v1/events"), SEND_VALIDATOR_LIVENESS("/eth/v1/validator/liveness/:epoch"), - BEACON_COMMITTEE_SELECTIONS("/eth/v1/validator/beacon_committee_selections"); + BEACON_COMMITTEE_SELECTIONS("/eth/v1/validator/beacon_committee_selections"), + SYNC_COMMITTEE_SELECTIONS("/eth/v1/validator/sync_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 147206afc00..c8adbfa788b 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 @@ -26,6 +26,7 @@ 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.ethereum.json.types.validator.SyncCommitteeSelectionProof; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.ssz.SszList; import tech.pegasys.teku.infrastructure.unsigned.UInt64; @@ -223,7 +224,13 @@ public SafeFuture>> getValidatorsLivenes @Override public SafeFuture>> getBeaconCommitteeSelectionProof( - final List request) { - return dutiesProviderChannel.getBeaconCommitteeSelectionProof(request); + final List requests) { + return dutiesProviderChannel.getBeaconCommitteeSelectionProof(requests); + } + + @Override + public SafeFuture>> getSyncCommitteeSelectionProof( + final List requests) { + return dutiesProviderChannel.getSyncCommitteeSelectionProof(requests); } } 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 241548c49fa..5a6c74a3f36 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 @@ -24,6 +24,7 @@ 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.SyncCommitteeSelectionProof; import tech.pegasys.teku.infrastructure.ssz.SszList; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; @@ -45,6 +46,7 @@ import tech.pegasys.teku.validator.remote.typedef.handlers.ProduceBlockRequest; import tech.pegasys.teku.validator.remote.typedef.handlers.RegisterValidatorsRequest; import tech.pegasys.teku.validator.remote.typedef.handlers.SendSignedBlockRequest; +import tech.pegasys.teku.validator.remote.typedef.handlers.SyncCommitteeSelectionsRequest; public class OkHttpValidatorTypeDefClient extends OkHttpValidatorMinimalTypeDefClient { @@ -60,6 +62,7 @@ public class OkHttpValidatorTypeDefClient extends OkHttpValidatorMinimalTypeDefC private final RegisterValidatorsRequest registerValidatorsRequest; private final CreateAttestationDataRequest createAttestationDataRequest; private final BeaconCommitteeSelectionsRequest beaconCommitteeSelectionsRequest; + private final SyncCommitteeSelectionsRequest syncCommitteeSelectionsRequest; public OkHttpValidatorTypeDefClient( final OkHttpClient okHttpClient, @@ -81,6 +84,8 @@ public OkHttpValidatorTypeDefClient( new CreateAttestationDataRequest(baseEndpoint, okHttpClient); this.beaconCommitteeSelectionsRequest = new BeaconCommitteeSelectionsRequest(baseEndpoint, okHttpClient); + this.syncCommitteeSelectionsRequest = + new SyncCommitteeSelectionsRequest(baseEndpoint, okHttpClient); } public SyncingStatus getSyncingStatus() { @@ -162,4 +167,9 @@ public Optional> getBeaconCommitteeSelection final List validatorsPartialProofs) { return beaconCommitteeSelectionsRequest.getSelectionProof(validatorsPartialProofs); } + + public Optional> getSyncCommitteeSelectionProof( + final List validatorsPartialProofs) { + return syncCommitteeSelectionsRequest.getSelectionProof(validatorsPartialProofs); + } } diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/SyncCommitteeSelectionsRequest.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/SyncCommitteeSelectionsRequest.java new file mode 100644 index 00000000000..c3facbae2bb --- /dev/null +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/SyncCommitteeSelectionsRequest.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.SyncCommitteeSelectionProof.SYNC_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.SyncCommitteeSelectionProof; +import tech.pegasys.teku.validator.remote.apiclient.ValidatorApiMethod; +import tech.pegasys.teku.validator.remote.typedef.ResponseHandler; + +public class SyncCommitteeSelectionsRequest extends AbstractTypeDefRequest { + + public SyncCommitteeSelectionsRequest( + final HttpUrl baseEndpoint, final OkHttpClient okHttpClient) { + super(baseEndpoint, okHttpClient); + } + + public Optional> getSelectionProof( + final List validatorsPartialProof) { + return postJson( + ValidatorApiMethod.SYNC_COMMITTEE_SELECTIONS, + Collections.emptyMap(), + validatorsPartialProof, + listOf(SYNC_COMMITTEE_SELECTION_PROOF), + new ResponseHandler<>( + SharedApiTypes.withDataWrapper( + "SyncCommitteeSelectionsResponse", listOf(SYNC_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 4814c65b76c..85752a767df 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 @@ -63,6 +63,7 @@ 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.json.types.validator.SyncCommitteeSelectionProof; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.async.StubAsyncRunner; import tech.pegasys.teku.infrastructure.async.Waiter; @@ -138,6 +139,33 @@ public void beaconCommitteeSelectionsRequest_ReturnBeaconCommitteeSelectionProof assertThat(responseProof.getSelectionProof()).isEqualTo(proof.getSelectionProof()); } + @Test + public void syncCommitteeSelectionsRequest_ReturnSyncCommitteeSelectionProof() { + final String blsSignatureHex = + dataStructureUtil.randomSignature().toBytesCompressed().toHexString(); + final SyncCommitteeSelectionProof proof = + new SyncCommitteeSelectionProof.Builder() + .validatorIndex(1) + .slot(ONE) + .subcommitteeIndex(2) + .selectionProof(blsSignatureHex) + .build(); + + when(typeDefClient.getSyncCommitteeSelectionProof(any())) + .thenReturn(Optional.of(List.of(proof))); + + final SafeFuture>> future = + apiHandler.getSyncCommitteeSelectionProof(List.of(proof)); + asyncRunner.executeQueuedActions(); + + final List response = unwrapToValue(future); + final SyncCommitteeSelectionProof responseProof = response.get(0); + assertThat(responseProof.getValidatorIndex()).isEqualTo(proof.getValidatorIndex()); + assertThat(responseProof.getSlot()).isEqualTo(proof.getSlot()); + assertThat(responseProof.getSubcommitteeIndex()).isEqualTo(proof.getSubcommitteeIndex()); + assertThat(responseProof.getSelectionProof()).isEqualTo(proof.getSelectionProof()); + } + @Test public void getsEndpoint() { assertThat(apiHandler.getEndpoint()).isEqualTo(endpoint);