diff --git a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/OkHttpValidatorTypeDefClientTest.java b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/OkHttpValidatorTypeDefClientTest.java index 64328576af2..f324a202205 100644 --- a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/OkHttpValidatorTypeDefClientTest.java +++ b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/OkHttpValidatorTypeDefClientTest.java @@ -27,13 +27,20 @@ import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NOT_FOUND; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NO_CONTENT; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.BUILDER_BOOST_FACTOR; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.COMMITTEE_INDEX; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.EPOCH; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.GRAFFITI; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_CONSENSUS_VERSION; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.PARAM_BROADCAST_VALIDATION; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.RANDAO_REVEAL; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.SLOT; import static tech.pegasys.teku.infrastructure.json.JsonUtil.serialize; import static tech.pegasys.teku.infrastructure.unsigned.UInt64.ONE; import static tech.pegasys.teku.spec.SpecMilestone.ALTAIR; import static tech.pegasys.teku.spec.SpecMilestone.BELLATRIX; import static tech.pegasys.teku.spec.SpecMilestone.ELECTRA; +import static tech.pegasys.teku.spec.SpecMilestone.PHASE0; import static tech.pegasys.teku.spec.config.SpecConfig.FAR_FUTURE_EPOCH; import com.fasterxml.jackson.core.JsonProcessingException; @@ -43,6 +50,7 @@ import java.util.Collection; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.Set; import okhttp3.mockwebserver.MockResponse; @@ -54,12 +62,15 @@ import org.junit.jupiter.api.TestTemplate; import tech.pegasys.teku.api.exceptions.RemoteServiceNotAvailableException; import tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus; +import tech.pegasys.teku.bls.BLSSignature; import tech.pegasys.teku.ethereum.json.types.beacon.StateValidatorData; import tech.pegasys.teku.ethereum.json.types.validator.AttesterDuties; import tech.pegasys.teku.ethereum.json.types.validator.AttesterDuty; import tech.pegasys.teku.ethereum.json.types.validator.SyncCommitteeDuties; import tech.pegasys.teku.ethereum.json.types.validator.SyncCommitteeDuty; import tech.pegasys.teku.ethereum.json.types.validator.SyncCommitteeSubnetSubscription; +import tech.pegasys.teku.infrastructure.http.RestApiConstants; +import tech.pegasys.teku.infrastructure.json.types.SerializableTypeDefinition; import tech.pegasys.teku.infrastructure.ssz.SszDataAssert; import tech.pegasys.teku.infrastructure.ssz.SszList; import tech.pegasys.teku.infrastructure.unsigned.UInt64; @@ -69,9 +80,12 @@ import tech.pegasys.teku.spec.datastructures.builder.SignedValidatorRegistration; import tech.pegasys.teku.spec.datastructures.metadata.ObjectAndMetaData; import tech.pegasys.teku.spec.datastructures.operations.Attestation; +import tech.pegasys.teku.spec.datastructures.operations.SignedAggregateAndProof; import tech.pegasys.teku.spec.datastructures.operations.versions.altair.SignedContributionAndProof; import tech.pegasys.teku.spec.datastructures.operations.versions.altair.SyncCommitteeContribution; import tech.pegasys.teku.spec.datastructures.operations.versions.altair.SyncCommitteeContributionSchema; +import tech.pegasys.teku.spec.datastructures.operations.versions.altair.SyncCommitteeMessage; +import tech.pegasys.teku.spec.datastructures.operations.versions.altair.SyncCommitteeMessageSchema; import tech.pegasys.teku.spec.datastructures.state.Validator; import tech.pegasys.teku.spec.datastructures.validator.BroadcastValidationLevel; import tech.pegasys.teku.spec.datastructures.validator.SubnetSubscription; @@ -150,15 +164,23 @@ void publishesBlindedBlockSszEncoded() throws InterruptedException { final SignedBeaconBlock signedBeaconBlock = dataStructureUtil.randomSignedBlindedBeaconBlock(); + final BroadcastValidationLevel broadcastValidationLevel = BroadcastValidationLevel.GOSSIP; final SendSignedBlockResult result = okHttpValidatorTypeDefClientWithPreferredSsz.sendSignedBlock( - signedBeaconBlock, BroadcastValidationLevel.GOSSIP); + signedBeaconBlock, broadcastValidationLevel); assertThat(result.isPublished()).isTrue(); final RecordedRequest recordedRequest = mockWebServer.takeRequest(); assertThat(recordedRequest.getBody().readByteArray()) .isEqualTo(signedBeaconBlock.sszSerialize().toArrayUnsafe()); + if (specMilestone.isLessThanOrEqualTo(ALTAIR)) { + assertThat(recordedRequest.getPath()) + .contains(ValidatorApiMethod.SEND_SIGNED_BLOCK_V2.getPath(emptyMap())); + } else { + assertThat(recordedRequest.getPath()) + .contains(ValidatorApiMethod.SEND_SIGNED_BLINDED_BLOCK_V2.getPath(emptyMap())); + } assertThat(recordedRequest.getRequestUrl().queryParameter(PARAM_BROADCAST_VALIDATION)) .isEqualTo("gossip"); assertThat(recordedRequest.getHeader(HEADER_CONSENSUS_VERSION)) @@ -186,13 +208,21 @@ void publishesBlindedBlockJsonEncoded() throws InterruptedException, JsonProcess .getSignedBlindedBlockContainerSchema() .getJsonTypeDefinition()); + if (specMilestone.isLessThanOrEqualTo(ALTAIR)) { + assertThat(recordedRequest.getPath()) + .contains(ValidatorApiMethod.SEND_SIGNED_BLOCK_V2.getPath(emptyMap())); + } else { + assertThat(recordedRequest.getPath()) + .contains(ValidatorApiMethod.SEND_SIGNED_BLINDED_BLOCK_V2.getPath(emptyMap())); + } + final String actualRequest = recordedRequest.getBody().readUtf8(); assertJsonEquals(actualRequest, expectedRequest); } @TestTemplate - void getsSyncingStatus() { + void getsSyncingStatus() throws InterruptedException { mockWebServer.enqueue( new MockResponse() .setResponseCode(200) @@ -208,6 +238,8 @@ void getsSyncingStatus() { final SyncingStatus result = typeDefClient.getSyncingStatus(); + final RecordedRequest request = mockWebServer.takeRequest(); + assertThat(result) .satisfies( syncingStatus -> { @@ -216,6 +248,33 @@ void getsSyncingStatus() { assertThat(syncingStatus.isSyncing()).isTrue(); assertThat(syncingStatus.getIsOptimistic()).hasValue(true); }); + assertThat(request.getPath()) + .contains(ValidatorApiMethod.GET_SYNCING_STATUS.getPath(emptyMap())); + } + + @TestTemplate + void getProposerDuties_shouldMakeExpectedRequest() throws InterruptedException { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NO_CONTENT)); + + final UInt64 epoch = dataStructureUtil.randomEpoch(); + typeDefClient.getProposerDuties(epoch); + + final RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getPath()) + .isEqualTo( + "/" + ValidatorApiMethod.GET_PROPOSER_DUTIES.getPath(Map.of(EPOCH, epoch.toString()))); + } + + @TestTemplate + void getPeerCount_shouldMakeExpectedRequest() throws InterruptedException { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NO_CONTENT)); + + typeDefClient.getPeerCount(); + + final RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getPath()).contains(ValidatorApiMethod.GET_PEER_COUNT.getPath(emptyMap())); } @TestTemplate @@ -313,7 +372,7 @@ void registerValidators_fallbacksToJsonIfSszNotSupported() throws InterruptedExc mockWebServer.enqueue(new MockResponse().setResponseCode(200)); mockWebServer.enqueue(new MockResponse().setResponseCode(200)); - SszList validatorRegistrations = + final SszList validatorRegistrations = dataStructureUtil.randomSignedValidatorRegistrations(5); sszRegisterValidatorsRequest.submit(validatorRegistrations); @@ -391,7 +450,7 @@ public void postValidators_whenSuccess_returnsResponse() throws JsonProcessingEx final String body = serialize(response, STATE_VALIDATORS_RESPONSE_TYPE); mockWebServer.enqueue(new MockResponse().setResponseCode(SC_OK).setBody(body)); - Optional> result = + final Optional> result = typeDefClient.postStateValidators(List.of("1", "2")); assertThat(result).isPresent(); @@ -433,7 +492,8 @@ public void postSyncDuties_whenSuccess_returnsResponse() final UInt64 epoch = ONE; final IntList validatorIndices = IntList.of(1, 2); - Optional result = typeDefClient.postSyncDuties(epoch, validatorIndices); + final Optional result = + typeDefClient.postSyncDuties(epoch, validatorIndices); final RecordedRequest recordedRequest = mockWebServer.takeRequest(); assertThat(recordedRequest.getPath()).isEqualTo("/eth/v1/validator/duties/sync/" + epoch); @@ -458,7 +518,8 @@ public void postAttesterDuties_whenSuccess_returnsResponse() final UInt64 epoch = ONE; final IntList validatorIndices = IntList.of(1, 2); - Optional result = typeDefClient.postAttesterDuties(epoch, validatorIndices); + final Optional result = + typeDefClient.postAttesterDuties(epoch, validatorIndices); final RecordedRequest recordedRequest = mockWebServer.takeRequest(); assertThat(recordedRequest.getPath()).isEqualTo("/eth/v1/validator/duties/attester/" + epoch); @@ -501,7 +562,7 @@ public void subscribeToPersistentSubnets_makesExpectedRequest() throws Exception typeDefClient.subscribeToPersistentSubnets(subnetSubscriptions); - RecordedRequest request = mockWebServer.takeRequest(); + final RecordedRequest request = mockWebServer.takeRequest(); assertThat(request.getMethod()).isEqualTo("POST"); assertThat(request.getPath()) @@ -563,7 +624,7 @@ public void subscribeToBeaconCommitteeForAggregation_makesExpectedRequest() thro new CommitteeSubscriptionRequest( validatorIndex2, committeeIndex2, committeesAtSlot2, slot2, aggregator2))); - RecordedRequest request = mockWebServer.takeRequest(); + final RecordedRequest request = mockWebServer.takeRequest(); assertThat(request.getMethod()).isEqualTo("POST"); assertThat(request.getPath()) @@ -680,7 +741,7 @@ public void createAggregate_makesExpectedRequest_preElectra() throws Exception { typeDefClient.createAggregate(slot, attestationHashTreeRoot, Optional.empty()); - RecordedRequest request = mockWebServer.takeRequest(); + final RecordedRequest request = mockWebServer.takeRequest(); assertThat(request.getMethod()).isEqualTo("GET"); assertThat(request.getPath()).contains(ValidatorApiMethod.GET_AGGREGATE.getPath(emptyMap())); @@ -700,7 +761,7 @@ public void createAggregate_makesExpectedRequest_postElectra() throws Exception typeDefClient.createAggregate( slot, attestationHashTreeRoot, Optional.of(dataStructureUtil.randomUInt64())); - RecordedRequest request = mockWebServer.takeRequest(); + final RecordedRequest request = mockWebServer.takeRequest(); assertThat(request.getMethod()).isEqualTo("GET"); assertThat(request.getPath()).contains(ValidatorApiMethod.GET_AGGREGATE_V2.getPath(emptyMap())); @@ -709,6 +770,211 @@ public void createAggregate_makesExpectedRequest_postElectra() throws Exception .isEqualTo(attestationHashTreeRoot.toHexString()); } + @TestTemplate + public void sendValidatorsLiveness_makesExpectedRequest() throws Exception { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NO_CONTENT)); + + final UInt64 epoch = dataStructureUtil.randomEpoch(); + final List validatorIndices = + List.of(dataStructureUtil.randomValidatorIndex(), dataStructureUtil.randomValidatorIndex()); + + typeDefClient.sendValidatorsLiveness(epoch, validatorIndices); + + final RecordedRequest request = mockWebServer.takeRequest(); + + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()) + .contains( + ValidatorApiMethod.SEND_VALIDATOR_LIVENESS.getPath( + Map.of(RestApiConstants.EPOCH, epoch.toString()))); + assertThat(request.getBody().readUtf8()) + .isEqualTo("[\"" + validatorIndices.get(0) + "\",\"" + validatorIndices.get(1) + "\"]"); + } + + @TestTemplate + public void sendSyncCommitteeMessages_makesExpectedRequest() throws Exception { + assumeThat(specMilestone).isGreaterThan(PHASE0); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_OK)); + + final UInt64 epoch = dataStructureUtil.randomEpoch(); + final List syncCommitteeMessages = + List.of( + dataStructureUtil.randomSyncCommitteeMessage(), + dataStructureUtil.randomSyncCommitteeMessage()); + + typeDefClient.sendSyncCommitteeMessages(syncCommitteeMessages); + + final RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()) + .contains( + ValidatorApiMethod.SEND_SYNC_COMMITTEE_MESSAGES.getPath( + Map.of(RestApiConstants.EPOCH, epoch.toString()))); + + final String expectedRequestPayloadBody = + serialize( + syncCommitteeMessages, + SerializableTypeDefinition.listOf( + SyncCommitteeMessageSchema.INSTANCE.getJsonTypeDefinition())); + assertThat(request.getBody().readString(StandardCharsets.UTF_8)) + .isEqualTo(expectedRequestPayloadBody); + } + + @TestTemplate + public void createAttestationData_makesExpectedRequest() throws Exception { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NO_CONTENT)); + + final UInt64 slot = dataStructureUtil.randomSlot(); + final int committeeIndex = dataStructureUtil.randomPositiveInt(); + + typeDefClient.createAttestationData(slot, committeeIndex); + + final RecordedRequest request = mockWebServer.takeRequest(); + + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getPath()) + .contains(ValidatorApiMethod.GET_ATTESTATION_DATA.getPath(emptyMap())); + assertThat(request.getRequestUrl().queryParameter(SLOT)).isEqualTo(slot.toString()); + assertThat(request.getRequestUrl().queryParameter(COMMITTEE_INDEX)) + .isEqualTo(String.valueOf(committeeIndex)); + } + + @TestTemplate + public void createUnsignedBlock_makesExpectedRequest() throws Exception { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NO_CONTENT)); + + final UInt64 slot = dataStructureUtil.randomSlot(); + final BLSSignature randaoReveal = dataStructureUtil.randomSignature(); + final Bytes32 graffiti = dataStructureUtil.randomBytes32(); + final UInt64 boostFactor = dataStructureUtil.randomUInt64(); + + typeDefClient.createUnsignedBlock( + slot, randaoReveal, Optional.of(graffiti), Optional.of(boostFactor)); + + final RecordedRequest request = mockWebServer.takeRequest(); + + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getPath()) + .contains(ValidatorApiMethod.GET_UNSIGNED_BLOCK_V3.getPath(Map.of(SLOT, slot.toString()))); + assertThat(request.getRequestUrl().queryParameter(RANDAO_REVEAL)) + .isEqualTo(randaoReveal.toString()); + assertThat(request.getRequestUrl().queryParameter(GRAFFITI)).isEqualTo(graffiti.toString()); + assertThat(request.getRequestUrl().queryParameter(BUILDER_BOOST_FACTOR)) + .isEqualTo(boostFactor.toString()); + } + + @TestTemplate + public void sendAggregate_makesExpectedRequest_preElectra() throws Exception { + assumeThat(specMilestone).isLessThan(ELECTRA); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_OK)); + + final List aggregateAndProofs = + List.of( + dataStructureUtil.randomSignedAggregateAndProof(), + dataStructureUtil.randomSignedAggregateAndProof()); + + typeDefClient.sendAggregateAndProofs(aggregateAndProofs); + final RecordedRequest request = mockWebServer.takeRequest(); + + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()) + .contains(ValidatorApiMethod.SEND_SIGNED_AGGREGATE_AND_PROOFS.getPath(emptyMap())); + + final String expectedRequestPayloadBody = + serialize( + aggregateAndProofs, + SerializableTypeDefinition.listOf( + spec.getGenesisSchemaDefinitions() + .getSignedAggregateAndProofSchema() + .getJsonTypeDefinition())); + + assertThat(request.getBody().readString(StandardCharsets.UTF_8)) + .isEqualTo(expectedRequestPayloadBody); + } + + @TestTemplate + public void sendAggregate_makesExpectedRequest_postElectra() throws Exception { + assumeThat(specMilestone).isGreaterThanOrEqualTo(ELECTRA); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NO_CONTENT)); + + final List aggregateAndProofs = + List.of( + dataStructureUtil.randomSignedAggregateAndProof(), + dataStructureUtil.randomSignedAggregateAndProof()); + + typeDefClient.sendAggregateAndProofs(aggregateAndProofs); + final RecordedRequest request = mockWebServer.takeRequest(); + + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()) + .contains(ValidatorApiMethod.SEND_SIGNED_AGGREGATE_AND_PROOFS_V2.getPath(emptyMap())); + + final String expectedRequestPayloadBody = + serialize( + aggregateAndProofs, + SerializableTypeDefinition.listOf( + spec.getGenesisSchemaDefinitions() + .getSignedAggregateAndProofSchema() + .getJsonTypeDefinition())); + + assertThat(request.getBody().readString(StandardCharsets.UTF_8)) + .isEqualTo(expectedRequestPayloadBody); + assertThat(request.getHeader(HEADER_CONSENSUS_VERSION)) + .isEqualTo(specMilestone.name().toLowerCase(Locale.ROOT)); + } + + @TestTemplate + public void sendSignedAttestation_makesExpectedRequest_preElectra() throws Exception { + assumeThat(specMilestone).isLessThan(ELECTRA); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NO_CONTENT)); + + final List attestations = + List.of(dataStructureUtil.randomAttestation(), dataStructureUtil.randomAttestation()); + + typeDefClient.sendSignedAttestations(attestations); + final RecordedRequest request = mockWebServer.takeRequest(); + + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()) + .contains(ValidatorApiMethod.SEND_SIGNED_ATTESTATION.getPath(emptyMap())); + + final String expectedRequestPayloadBody = + serialize( + attestations, + SerializableTypeDefinition.listOf( + spec.getGenesisSchemaDefinitions().getAttestationSchema().getJsonTypeDefinition())); + + assertThat(request.getBody().readString(StandardCharsets.UTF_8)) + .isEqualTo(expectedRequestPayloadBody); + } + + @TestTemplate + public void sendSignedAttestation_makesExpectedRequest_postElectra() throws Exception { + assumeThat(specMilestone).isGreaterThanOrEqualTo(ELECTRA); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NO_CONTENT)); + + final List attestations = + List.of(dataStructureUtil.randomAttestation(), dataStructureUtil.randomAttestation()); + + typeDefClient.sendSignedAttestations(attestations); + final RecordedRequest request = mockWebServer.takeRequest(); + + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()) + .contains(ValidatorApiMethod.SEND_SIGNED_ATTESTATION_V2.getPath(emptyMap())); + + final String expectedRequestPayloadBody = + serialize( + attestations, + SerializableTypeDefinition.listOf( + spec.getGenesisSchemaDefinitions().getAttestationSchema().getJsonTypeDefinition())); + + assertThat(request.getBody().readString(StandardCharsets.UTF_8)) + .isEqualTo(expectedRequestPayloadBody); + assertThat(request.getHeader(HEADER_CONSENSUS_VERSION)) + .isEqualTo(specMilestone.name().toLowerCase(Locale.ROOT)); + } + @TestTemplate public void createAggregate_whenBadParameters_throwsIllegalArgumentException() { final Bytes32 attestationHashTreeRoot = Bytes32.random(); diff --git a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/CreateSyncCommitteeContributionRequestTest.java b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/CreateSyncCommitteeContributionRequestTest.java new file mode 100644 index 00000000000..217749e991a --- /dev/null +++ b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/CreateSyncCommitteeContributionRequestTest.java @@ -0,0 +1,133 @@ +/* + * 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 org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assumptions.assumeThat; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_INTERNAL_SERVER_ERROR; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NOT_FOUND; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NO_CONTENT; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.BEACON_BLOCK_ROOT; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.SLOT; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.SUBCOMMITTEE_INDEX; +import static tech.pegasys.teku.infrastructure.json.JsonUtil.serialize; +import static tech.pegasys.teku.spec.SpecMilestone.PHASE0; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.Collections; +import java.util.Optional; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import tech.pegasys.teku.api.exceptions.RemoteServiceNotAvailableException; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.TestSpecContext; +import tech.pegasys.teku.spec.datastructures.operations.versions.altair.SyncCommitteeContribution; +import tech.pegasys.teku.spec.networks.Eth2Network; +import tech.pegasys.teku.validator.remote.apiclient.ValidatorApiMethod; +import tech.pegasys.teku.validator.remote.typedef.AbstractTypeDefRequestTestBase; + +@TestSpecContext(allMilestones = true, network = Eth2Network.MINIMAL) +public class CreateSyncCommitteeContributionRequestTest extends AbstractTypeDefRequestTestBase { + + private CreateSyncCommitteeContributionRequest request; + private UInt64 slot; + private int subcommitteeIndex; + private Bytes32 root; + + @BeforeEach + public void setup() { + request = + new CreateSyncCommitteeContributionRequest(mockWebServer.url("/"), okHttpClient, spec); + slot = dataStructureUtil.randomSlot(); + subcommitteeIndex = dataStructureUtil.randomPositiveInt(); + root = dataStructureUtil.randomBytes32(); + } + + @TestTemplate + public void createSyncCommitteeContribution_noRequestAtPhase0() { + assumeThat(specMilestone).isLessThanOrEqualTo(PHASE0); + request.submit(slot, subcommitteeIndex, root); + assertThat(mockWebServer.getRequestCount()).isZero(); + } + + @TestTemplate + public void createSyncCommitteeContribution_makesExpectedRequest() throws Exception { + assumeThat(specMilestone).isGreaterThan(PHASE0); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NO_CONTENT)); + request.submit(slot, subcommitteeIndex, root); + final RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getPath()) + .contains( + ValidatorApiMethod.GET_SYNC_COMMITTEE_CONTRIBUTION.getPath(Collections.emptyMap())); + assertThat(request.getRequestUrl().queryParameter(SLOT)).isEqualTo(slot.toString()); + assertThat(request.getRequestUrl().queryParameter(SUBCOMMITTEE_INDEX)) + .isEqualTo(String.valueOf(subcommitteeIndex)); + assertThat(request.getRequestUrl().queryParameter(BEACON_BLOCK_ROOT)) + .isEqualTo(String.valueOf(root.toHexString())); + } + + @TestTemplate + public void shouldGetSyncCommitteeContribution() throws JsonProcessingException { + assumeThat(specMilestone).isGreaterThan(PHASE0); + final SyncCommitteeContribution response = dataStructureUtil.randomSyncCommitteeContribution(); + final String jsonResponse = + serialize( + response, + spec.getGenesisSchemaDefinitions() + .toVersionAltair() + .orElseThrow() + .getSyncCommitteeContributionSchema() + .getJsonTypeDefinition()); + + mockWebServer.enqueue( + new MockResponse() + .setResponseCode(SC_OK) + .setBody(String.format("{\"data\":%s}", jsonResponse))); + + final Optional maybeSyncCommitteeContribution = + request.submit(slot, subcommitteeIndex, root); + assertThat(maybeSyncCommitteeContribution).isPresent(); + assertThat(maybeSyncCommitteeContribution.get()).isEqualTo(response); + } + + @TestTemplate + void handle400() { + assumeThat(specMilestone).isGreaterThan(PHASE0); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_BAD_REQUEST)); + assertThatThrownBy(() -> request.submit(slot, subcommitteeIndex, root)) + .isInstanceOf(IllegalArgumentException.class); + } + + @TestTemplate + void handle404() { + assumeThat(specMilestone).isGreaterThan(PHASE0); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NOT_FOUND)); + assertThat(request.submit(slot, subcommitteeIndex, root)).isEmpty(); + } + + @TestTemplate + void handle500() { + assumeThat(specMilestone).isGreaterThan(PHASE0); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_INTERNAL_SERVER_ERROR)); + assertThatThrownBy(() -> request.submit(slot, subcommitteeIndex, root)) + .isInstanceOf(RemoteServiceNotAvailableException.class); + } +} diff --git a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/GetStateValidatorsRequestTest.java b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/GetStateValidatorsRequestTest.java new file mode 100644 index 00000000000..d46b00546cf --- /dev/null +++ b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/GetStateValidatorsRequestTest.java @@ -0,0 +1,133 @@ +/* + * 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 org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assumptions.assumeThat; +import static tech.pegasys.teku.ethereum.json.types.beacon.StateValidatorDataBuilder.STATE_VALIDATORS_RESPONSE_TYPE; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_INTERNAL_SERVER_ERROR; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NOT_FOUND; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NO_CONTENT; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.PARAM_ID; +import static tech.pegasys.teku.infrastructure.json.JsonUtil.serialize; +import static tech.pegasys.teku.spec.SpecMilestone.PHASE0; +import static tech.pegasys.teku.spec.config.SpecConfig.FAR_FUTURE_EPOCH; + +import com.fasterxml.jackson.core.JsonProcessingException; +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.api.exceptions.RemoteServiceNotAvailableException; +import tech.pegasys.teku.api.response.v1.beacon.ValidatorStatus; +import tech.pegasys.teku.ethereum.json.types.beacon.StateValidatorData; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.TestSpecContext; +import tech.pegasys.teku.spec.datastructures.metadata.ObjectAndMetaData; +import tech.pegasys.teku.spec.datastructures.state.Validator; +import tech.pegasys.teku.spec.networks.Eth2Network; +import tech.pegasys.teku.validator.remote.apiclient.ValidatorApiMethod; +import tech.pegasys.teku.validator.remote.typedef.AbstractTypeDefRequestTestBase; + +@TestSpecContext(allMilestones = true, network = Eth2Network.MINIMAL) +public class GetStateValidatorsRequestTest extends AbstractTypeDefRequestTestBase { + + private GetStateValidatorsRequest request; + private List validatorIds; + + @BeforeEach + public void setup() { + request = new GetStateValidatorsRequest(mockWebServer.url("/"), okHttpClient); + validatorIds = + List.of( + dataStructureUtil.randomPublicKey().toHexString(), + dataStructureUtil.randomPublicKey().toHexString()); + } + + @TestTemplate + public void getStateValidatorsRequest_makesExpectedRequest() throws Exception { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NO_CONTENT)); + request.submit(validatorIds); + final RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getPath()) + .contains(ValidatorApiMethod.GET_VALIDATORS.getPath(Collections.emptyMap())); + assertThat(request.getRequestUrl().queryParameter(PARAM_ID)) + .isEqualTo(String.join(",", validatorIds)); + } + + @TestTemplate + public void shouldGetStateValidatorsData() throws JsonProcessingException { + final List expected = + List.of(generateStateValidatorData(), generateStateValidatorData()); + final ObjectAndMetaData> response = + new ObjectAndMetaData<>(expected, specMilestone, false, true, false); + + final String body = serialize(response, STATE_VALIDATORS_RESPONSE_TYPE); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_OK).setBody(body)); + + final Optional>> maybeStateValidatorsData = + request.submit(validatorIds); + assertThat(maybeStateValidatorsData).isPresent(); + assertThat(maybeStateValidatorsData.get().getData()).isEqualTo(expected); + } + + @TestTemplate + void handle400() { + assumeThat(specMilestone).isGreaterThan(PHASE0); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_BAD_REQUEST)); + assertThatThrownBy(() -> request.submit(validatorIds)) + .isInstanceOf(IllegalArgumentException.class); + } + + @TestTemplate + void handle404() { + assumeThat(specMilestone).isGreaterThan(PHASE0); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NOT_FOUND)); + assertThat(request.submit(validatorIds)).isEmpty(); + } + + @TestTemplate + void handle500() { + assumeThat(specMilestone).isGreaterThan(PHASE0); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_INTERNAL_SERVER_ERROR)); + assertThatThrownBy(() -> request.submit(validatorIds)) + .isInstanceOf(RemoteServiceNotAvailableException.class); + } + + private StateValidatorData generateStateValidatorData() { + final long index = dataStructureUtil.randomLong(); + final Validator validator = + new Validator( + dataStructureUtil.randomPublicKey(), + dataStructureUtil.randomBytes32(), + dataStructureUtil.randomUInt64(), + false, + UInt64.ZERO, + UInt64.ZERO, + FAR_FUTURE_EPOCH, + FAR_FUTURE_EPOCH); + return new StateValidatorData( + UInt64.valueOf(index), + dataStructureUtil.randomUInt64(), + ValidatorStatus.active_ongoing, + validator); + } +} diff --git a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/PostAttesterDutiesRequestTest.java b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/PostAttesterDutiesRequestTest.java new file mode 100644 index 00000000000..f01c87f9633 --- /dev/null +++ b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/PostAttesterDutiesRequestTest.java @@ -0,0 +1,91 @@ +/* + * 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 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_INTERNAL_SERVER_ERROR; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NOT_FOUND; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NO_CONTENT; +import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.INTEGER_TYPE; + +import java.util.List; +import java.util.Map; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import tech.pegasys.teku.api.exceptions.RemoteServiceNotAvailableException; +import tech.pegasys.teku.infrastructure.http.RestApiConstants; +import tech.pegasys.teku.infrastructure.json.JsonUtil; +import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition; +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(allMilestones = true, network = Eth2Network.MINIMAL) +public class PostAttesterDutiesRequestTest extends AbstractTypeDefRequestTestBase { + + private PostAttesterDutiesRequest request; + private UInt64 epoch; + private List validatorIndices; + + @BeforeEach + public void setup() { + request = new PostAttesterDutiesRequest(mockWebServer.url("/"), okHttpClient); + epoch = dataStructureUtil.randomEpoch(); + validatorIndices = + List.of( + dataStructureUtil.randomValidatorIndex().intValue(), + dataStructureUtil.randomValidatorIndex().intValue()); + } + + @TestTemplate + public void postAttesterDuties_makesExpectedRequest() throws Exception { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NO_CONTENT)); + request.submit(epoch, validatorIndices); + final RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()) + .contains( + ValidatorApiMethod.GET_ATTESTATION_DUTIES.getPath( + Map.of(RestApiConstants.EPOCH, epoch.toString()))); + final String requestBody = + JsonUtil.serialize(validatorIndices, DeserializableTypeDefinition.listOf(INTEGER_TYPE)); + assertThat(request.getBody().readUtf8()).isEqualTo(requestBody); + } + + @TestTemplate + void handle400() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_BAD_REQUEST)); + assertThatThrownBy(() -> request.submit(epoch, validatorIndices)) + .isInstanceOf(IllegalArgumentException.class); + } + + @TestTemplate + void handle404() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NOT_FOUND)); + assertThat(request.submit(epoch, validatorIndices)).isEmpty(); + } + + @TestTemplate + void handle500() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_INTERNAL_SERVER_ERROR)); + assertThatThrownBy(() -> request.submit(epoch, validatorIndices)) + .isInstanceOf(RemoteServiceNotAvailableException.class); + } +} diff --git a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/PostSyncDutiesRequestTest.java b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/PostSyncDutiesRequestTest.java new file mode 100644 index 00000000000..1b0db23b982 --- /dev/null +++ b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/PostSyncDutiesRequestTest.java @@ -0,0 +1,90 @@ +/* + * 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 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_INTERNAL_SERVER_ERROR; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NOT_FOUND; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NO_CONTENT; +import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.INTEGER_TYPE; + +import java.util.List; +import java.util.Map; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import tech.pegasys.teku.api.exceptions.RemoteServiceNotAvailableException; +import tech.pegasys.teku.infrastructure.http.RestApiConstants; +import tech.pegasys.teku.infrastructure.json.JsonUtil; +import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition; +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(allMilestones = true, network = Eth2Network.MINIMAL) +public class PostSyncDutiesRequestTest extends AbstractTypeDefRequestTestBase { + private PostSyncDutiesRequest request; + private UInt64 epoch; + private List validatorIndices; + + @BeforeEach + public void setup() { + request = new PostSyncDutiesRequest(mockWebServer.url("/"), okHttpClient); + epoch = dataStructureUtil.randomEpoch(); + validatorIndices = + List.of( + dataStructureUtil.randomValidatorIndex().intValue(), + dataStructureUtil.randomValidatorIndex().intValue()); + } + + @TestTemplate + public void postAttesterDuties_makesExpectedRequest() throws Exception { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NO_CONTENT)); + request.submit(epoch, validatorIndices); + final RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()) + .contains( + ValidatorApiMethod.GET_SYNC_COMMITTEE_DUTIES.getPath( + Map.of(RestApiConstants.EPOCH, epoch.toString()))); + final String requestBody = + JsonUtil.serialize(validatorIndices, DeserializableTypeDefinition.listOf(INTEGER_TYPE)); + assertThat(request.getBody().readUtf8()).isEqualTo(requestBody); + } + + @TestTemplate + void handle400() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_BAD_REQUEST)); + assertThatThrownBy(() -> request.submit(epoch, validatorIndices)) + .isInstanceOf(IllegalArgumentException.class); + } + + @TestTemplate + void handle404() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NOT_FOUND)); + assertThat(request.submit(epoch, validatorIndices)).isEmpty(); + } + + @TestTemplate + void handle500() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_INTERNAL_SERVER_ERROR)); + assertThatThrownBy(() -> request.submit(epoch, validatorIndices)) + .isInstanceOf(RemoteServiceNotAvailableException.class); + } +} diff --git a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/PrepareBeaconProposersRequestTest.java b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/PrepareBeaconProposersRequestTest.java new file mode 100644 index 00000000000..0d1cf6dcec2 --- /dev/null +++ b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/PrepareBeaconProposersRequestTest.java @@ -0,0 +1,85 @@ +/* + * 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 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_INTERNAL_SERVER_ERROR; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; + +import java.util.Collections; +import java.util.List; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import tech.pegasys.teku.api.exceptions.RemoteServiceNotAvailableException; +import tech.pegasys.teku.infrastructure.json.JsonUtil; +import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition; +import tech.pegasys.teku.spec.TestSpecContext; +import tech.pegasys.teku.spec.datastructures.validator.BeaconPreparableProposer; +import tech.pegasys.teku.spec.networks.Eth2Network; +import tech.pegasys.teku.validator.remote.apiclient.ValidatorApiMethod; +import tech.pegasys.teku.validator.remote.typedef.AbstractTypeDefRequestTestBase; + +@TestSpecContext(allMilestones = true, network = Eth2Network.MINIMAL) +public class PrepareBeaconProposersRequestTest extends AbstractTypeDefRequestTestBase { + private PrepareBeaconProposersRequest request; + private List beaconPreparableProposers; + + @BeforeEach + public void setup() { + request = new PrepareBeaconProposersRequest(mockWebServer.url("/"), okHttpClient); + beaconPreparableProposers = + List.of( + dataStructureUtil.randomBeaconPreparableProposer(), + dataStructureUtil.randomBeaconPreparableProposer()); + } + + @TestTemplate + public void postAttesterDuties_makesExpectedRequest() throws Exception { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_OK)); + request.submit(beaconPreparableProposers); + final RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()) + .contains(ValidatorApiMethod.PREPARE_BEACON_PROPOSER.getPath(Collections.emptyMap())); + final String requestBody = + JsonUtil.serialize( + beaconPreparableProposers, + DeserializableTypeDefinition.listOf(BeaconPreparableProposer.SSZ_DATA)); + assertThat(request.getBody().readUtf8()).isEqualTo(requestBody); + } + + @TestTemplate + public void prepareBeaconProposer_noRequestWhenEmptyList() { + request.submit(List.of()); + assertThat(mockWebServer.getRequestCount()).isEqualTo(0); + } + + @TestTemplate + void handle400() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_BAD_REQUEST)); + assertThatThrownBy(() -> request.submit(beaconPreparableProposers)) + .isInstanceOf(IllegalArgumentException.class); + } + + @TestTemplate + void handle500() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_INTERNAL_SERVER_ERROR)); + assertThatThrownBy(() -> request.submit(beaconPreparableProposers)) + .isInstanceOf(RemoteServiceNotAvailableException.class); + } +} diff --git a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/RegisterValidatorsRequestTest.java b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/RegisterValidatorsRequestTest.java new file mode 100644 index 00000000000..306bac622c0 --- /dev/null +++ b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/RegisterValidatorsRequestTest.java @@ -0,0 +1,93 @@ +/* + * 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 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_INTERNAL_SERVER_ERROR; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; + +import java.util.Collections; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import tech.pegasys.teku.api.exceptions.RemoteServiceNotAvailableException; +import tech.pegasys.teku.infrastructure.json.JsonUtil; +import tech.pegasys.teku.infrastructure.ssz.SszList; +import tech.pegasys.teku.spec.TestSpecContext; +import tech.pegasys.teku.spec.datastructures.builder.SignedValidatorRegistration; +import tech.pegasys.teku.spec.networks.Eth2Network; +import tech.pegasys.teku.spec.schemas.ApiSchemas; +import tech.pegasys.teku.validator.remote.apiclient.ValidatorApiMethod; +import tech.pegasys.teku.validator.remote.typedef.AbstractTypeDefRequestTestBase; + +@TestSpecContext(allMilestones = true, network = Eth2Network.MINIMAL) +public class RegisterValidatorsRequestTest extends AbstractTypeDefRequestTestBase { + + private RegisterValidatorsRequest request; + private SszList validatorRegistrations; + + @BeforeEach + public void setup() { + request = new RegisterValidatorsRequest(mockWebServer.url("/"), okHttpClient, true); + validatorRegistrations = dataStructureUtil.randomSignedValidatorRegistrations(10); + } + + @TestTemplate + public void postAttesterDuties_makesExpectedRequestAsSsz() throws Exception { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_OK)); + request.submit(validatorRegistrations); + final RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()) + .contains(ValidatorApiMethod.REGISTER_VALIDATOR.getPath(Collections.emptyMap())); + final byte[] requestBody = + ApiSchemas.SIGNED_VALIDATOR_REGISTRATIONS_SCHEMA + .sszSerialize(validatorRegistrations) + .toArray(); + assertThat(request.getBody().readByteArray()).isEqualTo(requestBody); + } + + @TestTemplate + public void postAttesterDuties_makesExpectedRequestAsJson() throws Exception { + request = new RegisterValidatorsRequest(mockWebServer.url("/"), okHttpClient, false); + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_OK)); + request.submit(validatorRegistrations); + final RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()) + .contains(ValidatorApiMethod.REGISTER_VALIDATOR.getPath(Collections.emptyMap())); + final String requestBody = + JsonUtil.serialize( + validatorRegistrations, + ApiSchemas.SIGNED_VALIDATOR_REGISTRATIONS_SCHEMA.getJsonTypeDefinition()); + assertThat(request.getBody().readUtf8()).isEqualTo(requestBody); + } + + @TestTemplate + void handle400() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_BAD_REQUEST)); + assertThatThrownBy(() -> request.submit(validatorRegistrations)) + .isInstanceOf(IllegalArgumentException.class); + } + + @TestTemplate + void handle500() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_INTERNAL_SERVER_ERROR)); + assertThatThrownBy(() -> request.submit(validatorRegistrations)) + .isInstanceOf(RemoteServiceNotAvailableException.class); + } +} diff --git a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendAggregatesAndProofsRequestTest.java b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendAggregatesAndProofsRequestTest.java index 3b568906691..7b2a269cd6b 100644 --- a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendAggregatesAndProofsRequestTest.java +++ b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendAggregatesAndProofsRequestTest.java @@ -19,6 +19,7 @@ import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_INTERNAL_SERVER_ERROR; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_CONSENSUS_VERSION; import com.fasterxml.jackson.core.JsonProcessingException; import java.util.List; @@ -74,6 +75,8 @@ void handle200() throws InterruptedException, JsonProcessingException { .contains(ValidatorApiMethod.SEND_SIGNED_AGGREGATE_AND_PROOFS_V2.getPath(emptyMap())); assertThat(recordedRequest.getHeader(RestApiConstants.HEADER_CONSENSUS_VERSION)) .isEqualTo(specMilestone.name().toLowerCase(Locale.ROOT)); + assertThat(recordedRequest.getHeader(HEADER_CONSENSUS_VERSION)) + .isEqualTo(specMilestone.name().toLowerCase(Locale.ROOT)); } else { assertThat(recordedRequest.getPath()) .contains(ValidatorApiMethod.SEND_SIGNED_AGGREGATE_AND_PROOFS.getPath(emptyMap())); @@ -99,8 +102,7 @@ void handle400() { } @TestTemplate - void shouldUseV2ApiWhenUseAttestationsV2ApisEnabled() - throws InterruptedException, JsonProcessingException { + void shouldUseV2ApiWhenUseAttestationsV2ApisEnabled() throws InterruptedException { this.request = new SendAggregateAndProofsRequest(mockWebServer.url("/"), okHttpClient, true, spec); mockWebServer.enqueue(new MockResponse().setResponseCode(SC_OK)); diff --git a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendContributionAndProofsRequestTest.java b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendContributionAndProofsRequestTest.java new file mode 100644 index 00000000000..03fe13a5b2f --- /dev/null +++ b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendContributionAndProofsRequestTest.java @@ -0,0 +1,88 @@ +/* + * 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 org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assumptions.assumeThat; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_INTERNAL_SERVER_ERROR; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NO_CONTENT; +import static tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition.listOf; +import static tech.pegasys.teku.spec.SpecMilestone.PHASE0; + +import java.util.Collections; +import java.util.List; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import tech.pegasys.teku.api.exceptions.RemoteServiceNotAvailableException; +import tech.pegasys.teku.infrastructure.json.JsonUtil; +import tech.pegasys.teku.spec.TestSpecContext; +import tech.pegasys.teku.spec.datastructures.operations.versions.altair.SignedContributionAndProof; +import tech.pegasys.teku.spec.networks.Eth2Network; +import tech.pegasys.teku.validator.remote.apiclient.ValidatorApiMethod; +import tech.pegasys.teku.validator.remote.typedef.AbstractTypeDefRequestTestBase; + +@TestSpecContext(allMilestones = true, network = Eth2Network.MINIMAL) +public class SendContributionAndProofsRequestTest extends AbstractTypeDefRequestTestBase { + private SendContributionAndProofsRequest request; + private List contributionAndProofs; + + @BeforeEach + public void setup() { + assumeThat(specMilestone).isGreaterThan(PHASE0); + request = new SendContributionAndProofsRequest(mockWebServer.url("/"), okHttpClient); + contributionAndProofs = + List.of( + dataStructureUtil.randomSignedContributionAndProof(), + dataStructureUtil.randomSignedContributionAndProof()); + } + + @TestTemplate + public void postAttesterDuties_makesExpectedRequest() throws Exception { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NO_CONTENT)); + request.submit(contributionAndProofs); + final RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()) + .contains(ValidatorApiMethod.SEND_CONTRIBUTION_AND_PROOF.getPath(Collections.emptyMap())); + final String requestBody = + JsonUtil.serialize( + contributionAndProofs, + listOf(contributionAndProofs.getFirst().getSchema().getJsonTypeDefinition())); + assertThat(request.getBody().readUtf8()).isEqualTo(requestBody); + } + + @TestTemplate + void noRequestWhenEmptyList() { + request.submit(List.of()); + assertThat(mockWebServer.getRequestCount()).isZero(); + } + + @TestTemplate + void handle400() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_BAD_REQUEST)); + assertThatThrownBy(() -> request.submit(contributionAndProofs)) + .isInstanceOf(IllegalArgumentException.class); + } + + @TestTemplate + void handle500() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_INTERNAL_SERVER_ERROR)); + assertThatThrownBy(() -> request.submit(contributionAndProofs)) + .isInstanceOf(RemoteServiceNotAvailableException.class); + } +} diff --git a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendSubscribeToSyncCommitteeSubnetsRequestTest.java b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendSubscribeToSyncCommitteeSubnetsRequestTest.java new file mode 100644 index 00000000000..9e04a5b53b4 --- /dev/null +++ b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendSubscribeToSyncCommitteeSubnetsRequestTest.java @@ -0,0 +1,76 @@ +/* + * 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 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_INTERNAL_SERVER_ERROR; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NO_CONTENT; + +import it.unimi.dsi.fastutil.ints.IntSet; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import tech.pegasys.teku.api.exceptions.RemoteServiceNotAvailableException; +import tech.pegasys.teku.ethereum.json.types.validator.SyncCommitteeSubnetSubscription; +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(allMilestones = true, network = Eth2Network.MINIMAL) +public class SendSubscribeToSyncCommitteeSubnetsRequestTest extends AbstractTypeDefRequestTestBase { + private SendSubscribeToSyncCommitteeSubnetsRequest request; + final Collection subscriptions = + List.of(new SyncCommitteeSubnetSubscription(0, IntSet.of(1), UInt64.ZERO)); + + @BeforeEach + public void setup() { + request = new SendSubscribeToSyncCommitteeSubnetsRequest(mockWebServer.url("/"), okHttpClient); + } + + @TestTemplate + public void postAttesterDuties_makesExpectedRequest() throws Exception { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NO_CONTENT)); + request.submit(subscriptions); + final RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()) + .contains( + ValidatorApiMethod.SUBSCRIBE_TO_SYNC_COMMITTEE_SUBNET.getPath(Collections.emptyMap())); + assertThat(request.getBody().readUtf8()) + .isEqualTo( + "[{\"validator_index\":\"0\",\"sync_committee_indices\":[\"1\"],\"until_epoch\":\"0\"}]"); + } + + @TestTemplate + void handle400() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_BAD_REQUEST)); + assertThatThrownBy(() -> request.submit(subscriptions)) + .isInstanceOf(IllegalArgumentException.class); + } + + @TestTemplate + void handle500() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_INTERNAL_SERVER_ERROR)); + assertThatThrownBy(() -> request.submit(subscriptions)) + .isInstanceOf(RemoteServiceNotAvailableException.class); + } +} diff --git a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendSyncCommitteeMessagesRequestTest.java b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendSyncCommitteeMessagesRequestTest.java index 98ded9db926..8fc75a6ba49 100644 --- a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendSyncCommitteeMessagesRequestTest.java +++ b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendSyncCommitteeMessagesRequestTest.java @@ -66,6 +66,13 @@ void handle200() throws InterruptedException, JsonProcessingException { .contains(ValidatorApiMethod.SEND_SYNC_COMMITTEE_MESSAGES.getPath(emptyMap())); } + @TestTemplate + void shouldNotMakeRequestWhenEmptyMessages() { + final List response = request.submit(List.of()); + assertThat(response).isEmpty(); + assertThat(mockWebServer.getRequestCount()).isZero(); + } + @TestTemplate void handle400() { mockWebServer.enqueue( diff --git a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SubscribeToBeaconCommitteeRequestTest.java b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SubscribeToBeaconCommitteeRequestTest.java new file mode 100644 index 00000000000..345c4b86d1d --- /dev/null +++ b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SubscribeToBeaconCommitteeRequestTest.java @@ -0,0 +1,92 @@ +/* + * 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 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_INTERNAL_SERVER_ERROR; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NO_CONTENT; + +import java.util.Collections; +import java.util.List; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import tech.pegasys.teku.api.exceptions.RemoteServiceNotAvailableException; +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.api.CommitteeSubscriptionRequest; +import tech.pegasys.teku.validator.remote.apiclient.ValidatorApiMethod; +import tech.pegasys.teku.validator.remote.typedef.AbstractTypeDefRequestTestBase; + +@TestSpecContext(allMilestones = true, network = Eth2Network.MINIMAL) +public class SubscribeToBeaconCommitteeRequestTest extends AbstractTypeDefRequestTestBase { + private SubscribeToBeaconCommitteeRequest request; + private List subscriptions; + final int committeeIndex1 = 1; + final int validatorIndex1 = 6; + final UInt64 committeesAtSlot1 = UInt64.valueOf(10); + final UInt64 slot1 = UInt64.valueOf(15); + final boolean aggregator1 = true; + + final int committeeIndex2 = 2; + final int validatorIndex2 = 7; + final UInt64 committeesAtSlot2 = UInt64.valueOf(11); + final UInt64 slot2 = UInt64.valueOf(16); + final boolean aggregator2 = false; + + @BeforeEach + public void setup() { + request = new SubscribeToBeaconCommitteeRequest(mockWebServer.url("/"), okHttpClient); + subscriptions = + List.of( + new CommitteeSubscriptionRequest( + validatorIndex1, committeeIndex1, committeesAtSlot1, slot1, aggregator1), + new CommitteeSubscriptionRequest( + validatorIndex2, committeeIndex2, committeesAtSlot2, slot2, aggregator2)); + } + + @TestTemplate + public void postAttesterDuties_makesExpectedRequest() throws Exception { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NO_CONTENT)); + request.submit(subscriptions); + final RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()) + .contains( + ValidatorApiMethod.SUBSCRIBE_TO_BEACON_COMMITTEE_SUBNET.getPath( + Collections.emptyMap())); + final String expectedBody = + "[{\"validator_index\":\"6\",\"committee_index\":\"1\",\"committees_at_slot\":\"10\",\"slot\":\"15\",\"is_aggregator\":true}," + + "{\"validator_index\":\"7\",\"committee_index\":\"2\",\"committees_at_slot\":\"11\",\"slot\":\"16\",\"is_aggregator\":false}]"; + assertThat(request.getBody().readUtf8()).isEqualTo(expectedBody); + } + + @TestTemplate + void handle400() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_BAD_REQUEST)); + assertThatThrownBy(() -> request.submit(subscriptions)) + .isInstanceOf(IllegalArgumentException.class); + } + + @TestTemplate + void handle500() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_INTERNAL_SERVER_ERROR)); + assertThatThrownBy(() -> request.submit(subscriptions)) + .isInstanceOf(RemoteServiceNotAvailableException.class); + } +} diff --git a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SubscribeToPersistentSubnetsRequestTest.java b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SubscribeToPersistentSubnetsRequestTest.java new file mode 100644 index 00000000000..dfe564dab94 --- /dev/null +++ b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/SubscribeToPersistentSubnetsRequestTest.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.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_INTERNAL_SERVER_ERROR; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NO_CONTENT; + +import java.util.Collections; +import java.util.List; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import tech.pegasys.teku.api.exceptions.RemoteServiceNotAvailableException; +import tech.pegasys.teku.spec.TestSpecContext; +import tech.pegasys.teku.spec.datastructures.validator.SubnetSubscription; +import tech.pegasys.teku.spec.networks.Eth2Network; +import tech.pegasys.teku.validator.remote.apiclient.ValidatorApiMethod; +import tech.pegasys.teku.validator.remote.typedef.AbstractTypeDefRequestTestBase; + +@TestSpecContext(allMilestones = true, network = Eth2Network.MINIMAL) +public class SubscribeToPersistentSubnetsRequestTest extends AbstractTypeDefRequestTestBase { + private SubscribeToPersistentSubnetsRequest request; + private List subnetSubscriptions; + + @BeforeEach + public void setup() { + request = new SubscribeToPersistentSubnetsRequest(mockWebServer.url("/"), okHttpClient); + subnetSubscriptions = + List.of( + new SubnetSubscription( + dataStructureUtil.randomPositiveInt(64), dataStructureUtil.randomSlot())); + } + + @TestTemplate + public void postAttesterDuties_makesExpectedRequest() throws Exception { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_NO_CONTENT)); + request.submit(subnetSubscriptions); + final RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()) + .contains( + ValidatorApiMethod.SUBSCRIBE_TO_PERSISTENT_SUBNETS.getPath(Collections.emptyMap())); + assertThat(request.getBody().readUtf8()) + .isEqualTo("[{\"subnet_id\":\"35\",\"unsubscription_slot\":\"24752339414\"}]"); + } + + @TestTemplate + void handle400() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_BAD_REQUEST)); + assertThatThrownBy(() -> request.submit(subnetSubscriptions)) + .isInstanceOf(IllegalArgumentException.class); + } + + @TestTemplate + void handle500() { + mockWebServer.enqueue(new MockResponse().setResponseCode(SC_INTERNAL_SERVER_ERROR)); + assertThatThrownBy(() -> request.submit(subnetSubscriptions)) + .isInstanceOf(RemoteServiceNotAvailableException.class); + } +} diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/CreateAttestationDataRequest.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/CreateAttestationDataRequest.java index 1f0020ecce9..5a62b43ec36 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/CreateAttestationDataRequest.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/CreateAttestationDataRequest.java @@ -14,6 +14,8 @@ package tech.pegasys.teku.validator.remote.typedef.handlers; import static tech.pegasys.teku.ethereum.json.types.SharedApiTypes.withDataWrapper; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.COMMITTEE_INDEX; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.SLOT; import java.util.HashMap; import java.util.Map; @@ -33,8 +35,8 @@ public CreateAttestationDataRequest(final HttpUrl baseEndpoint, final OkHttpClie public Optional submit(final UInt64 slot, final int committeeIndex) { final Map queryParams = new HashMap<>(); - queryParams.put("slot", slot.toString()); - queryParams.put("committee_index", Integer.toString(committeeIndex)); + queryParams.put(SLOT, slot.toString()); + queryParams.put(COMMITTEE_INDEX, Integer.toString(committeeIndex)); return get( ValidatorApiMethod.GET_ATTESTATION_DATA, queryParams, diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/CreateSyncCommitteeContributionRequest.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/CreateSyncCommitteeContributionRequest.java index a065d654993..a72d6296c0b 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/CreateSyncCommitteeContributionRequest.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/CreateSyncCommitteeContributionRequest.java @@ -22,6 +22,7 @@ import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import org.apache.tuweni.bytes.Bytes32; +import tech.pegasys.teku.infrastructure.http.RestApiConstants; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.SpecMilestone; @@ -49,11 +50,11 @@ public Optional submit( .getSyncCommitteeContributionSchema(); final Map queryParams = Map.of( - "slot", + RestApiConstants.SLOT, slot.toString(), - "subcommittee_index", + RestApiConstants.SUBCOMMITTEE_INDEX, Integer.toString(subcommitteeIndex), - "beacon_block_root", + RestApiConstants.BEACON_BLOCK_ROOT, beaconBlockRoot.toHexString()); return get( GET_SYNC_COMMITTEE_CONTRIBUTION, diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/PostAttesterDutiesRequest.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/PostAttesterDutiesRequest.java index 0b63c07a6c7..3407b7dce5f 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/PostAttesterDutiesRequest.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/PostAttesterDutiesRequest.java @@ -23,6 +23,7 @@ import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import tech.pegasys.teku.ethereum.json.types.validator.AttesterDuties; +import tech.pegasys.teku.infrastructure.http.RestApiConstants; import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.validator.remote.typedef.ResponseHandler; @@ -36,7 +37,7 @@ public Optional submit( final UInt64 epoch, final Collection validatorIndices) { return postJson( GET_ATTESTATION_DUTIES, - Map.of("epoch", epoch.toString()), + Map.of(RestApiConstants.EPOCH, epoch.toString()), validatorIndices.stream().toList(), DeserializableTypeDefinition.listOf(INTEGER_TYPE, 1), new ResponseHandler<>(ATTESTER_DUTIES_RESPONSE_TYPE)); diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/PostSyncDutiesRequest.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/PostSyncDutiesRequest.java index 87daeb9384e..023110760d7 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/PostSyncDutiesRequest.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/PostSyncDutiesRequest.java @@ -24,6 +24,7 @@ import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import tech.pegasys.teku.ethereum.json.types.validator.SyncCommitteeDuties; +import tech.pegasys.teku.infrastructure.http.RestApiConstants; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.validator.remote.typedef.ResponseHandler; @@ -36,7 +37,7 @@ public Optional submit( final UInt64 epoch, final Collection validatorIndices) { return postJson( GET_SYNC_COMMITTEE_DUTIES, - Map.of("epoch", epoch.toString()), + Map.of(RestApiConstants.EPOCH, epoch.toString()), validatorIndices.stream().toList(), listOf(INTEGER_TYPE, 1), new ResponseHandler<>(SYNC_COMMITTEE_DUTIES_TYPE)); diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendValidatorLivenessRequest.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendValidatorLivenessRequest.java index fde71c9fc44..4daabcb531c 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendValidatorLivenessRequest.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/SendValidatorLivenessRequest.java @@ -25,6 +25,7 @@ import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import tech.pegasys.teku.api.migrated.ValidatorLivenessAtEpoch; +import tech.pegasys.teku.infrastructure.http.RestApiConstants; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.validator.remote.typedef.ResponseHandler; @@ -38,7 +39,7 @@ public Optional> submit( final UInt64 epoch, final List validatorIndices) { return postJson( SEND_VALIDATOR_LIVENESS, - Map.of("epoch", epoch.toString()), + Map.of(RestApiConstants.EPOCH, epoch.toString()), Collections.emptyMap(), Collections.emptyMap(), validatorIndices,