diff --git a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/AbstractGetNewBlockTest.java b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/AbstractGetNewBlockTest.java index aa6a9abeaf3..a9631d248bc 100644 --- a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/AbstractGetNewBlockTest.java +++ b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/AbstractGetNewBlockTest.java @@ -31,7 +31,10 @@ import tech.pegasys.teku.infrastructure.restapi.endpoints.RestApiEndpoint; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.TestSpecFactory; import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlock; +import tech.pegasys.teku.spec.datastructures.blocks.versions.deneb.BlindedBlockContents; +import tech.pegasys.teku.spec.util.DataStructureUtil; import tech.pegasys.teku.storage.client.ChainDataUnavailableException; public abstract class AbstractGetNewBlockTest extends AbstractMigratedBeaconHandlerTest { @@ -63,6 +66,20 @@ void shouldReturnBlockWithoutGraffiti() throws Exception { Resources.getResource(AbstractGetNewBlockTest.class, "beaconBlock.json"), UTF_8)); } + @Test + void shouldReturnBlockContentsPostDeneb() throws Exception { + spec = TestSpecFactory.createMinimalDeneb(); + DataStructureUtil denebData = new DataStructureUtil(spec); + final BlindedBlockContents blindedBlockContents = denebData.randomBlindedBlockContents(ONE); + doReturn(SafeFuture.completedFuture(Optional.of(blindedBlockContents))) + .when(validatorDataProvider) + .getUnsignedBeaconBlockAtSlot(ONE, signature, Optional.empty(), isBlindedBlocks()); + + handler.handleRequest(request); + + assertThat(request.getResponseBody()).isEqualTo(blindedBlockContents); + } + @Test void shouldThrowExceptionWithEmptyBlock() throws Exception { diff --git a/data/provider/src/test/java/tech/pegasys/teku/api/ValidatorDataProviderTest.java b/data/provider/src/test/java/tech/pegasys/teku/api/ValidatorDataProviderTest.java index 3a20ac8a32c..e99b4e8338e 100644 --- a/data/provider/src/test/java/tech/pegasys/teku/api/ValidatorDataProviderTest.java +++ b/data/provider/src/test/java/tech/pegasys/teku/api/ValidatorDataProviderTest.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assumptions.assumeThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -81,6 +82,7 @@ public class ValidatorDataProviderTest { private final JsonProvider jsonProvider = new JsonProvider(); private Spec spec; + private SpecMilestone specMilestone; private DataStructureUtil dataStructureUtil; private SchemaObjectProvider schemaProvider; private final CombinedChainDataClient combinedChainDataClient = @@ -98,6 +100,7 @@ public void setup(SpecContext specContext) { schemaProvider = new SchemaObjectProvider(spec); provider = new ValidatorDataProvider(spec, validatorApiChannel, combinedChainDataClient); blockInternal = dataStructureUtil.randomBeaconBlock(123); + specMilestone = specContext.getSpecMilestone(); } @TestTemplate @@ -133,40 +136,40 @@ void getUnsignedBeaconBlockAtSlot_shouldThrowIfFarFutureSlotRequested() { } @TestTemplate - void getUnsignedBeaconBlockAtSlot_shouldCreateAnUnsignedBlock() { + void getUnsignedBeaconBlockAtSlot_PreDeneb_shouldCreateAnUnsignedBlock() { + assumeThat(specMilestone).isLessThan(SpecMilestone.DENEB); when(combinedChainDataClient.getCurrentSlot()).thenReturn(ZERO); when(validatorApiChannel.createUnsignedBlock(ONE, signatureInternal, Optional.empty(), false)) .thenReturn(completedFuture(Optional.of(blockInternal))); - if (denebMilestoneReached()) { - blockContents = dataStructureUtil.randomBlockContents(); - when(validatorApiChannel.createUnsignedBlockContents( - ONE, signatureInternal, Optional.empty())) - .thenReturn(completedFuture(Optional.of(blockContents))); - } + SafeFuture> data = + provider.getUnsignedBeaconBlockAtSlot(ONE, signatureInternal, Optional.empty()); + + verify(validatorApiChannel) + .createUnsignedBlock(ONE, signatureInternal, Optional.empty(), false); + + assertThat(data).isCompleted(); + + assertThat(data.getNow(null).orElseThrow()).usingRecursiveComparison().isEqualTo(blockInternal); + } + + @TestTemplate + void getUnsignedBlockContentsAtSlot_PostDeneb_shouldCreateAnUnsignedBlockContents() { + assumeThat(specMilestone).isGreaterThanOrEqualTo(SpecMilestone.DENEB); + when(combinedChainDataClient.getCurrentSlot()).thenReturn(ZERO); + blockContents = dataStructureUtil.randomBlockContents(); + when(validatorApiChannel.createUnsignedBlockContents(ONE, signatureInternal, Optional.empty())) + .thenReturn(completedFuture(Optional.of(blockContents))); SafeFuture> data = provider.getUnsignedBeaconBlockAtSlot(ONE, signatureInternal, Optional.empty()); - if (denebMilestoneReached()) { - verify(validatorApiChannel) - .createUnsignedBlockContents(ONE, signatureInternal, Optional.empty()); - } else { - verify(validatorApiChannel) - .createUnsignedBlock(ONE, signatureInternal, Optional.empty(), false); - } + verify(validatorApiChannel) + .createUnsignedBlockContents(ONE, signatureInternal, Optional.empty()); assertThat(data).isCompleted(); - if (denebMilestoneReached()) { - assertThat(data.getNow(null).orElseThrow()) - .usingRecursiveComparison() - .isEqualTo(blockContents); - } else { - assertThat(data.getNow(null).orElseThrow()) - .usingRecursiveComparison() - .isEqualTo(blockInternal); - } + assertThat(data.getNow(null).orElseThrow()).usingRecursiveComparison().isEqualTo(blockContents); } @TestTemplate @@ -401,10 +404,4 @@ public void getAttesterDuties_shouldReturnDutiesForKnownValidator() { final AttesterDuties list = maybeList.orElseThrow(); assertThat(list.getDuties()).containsExactlyInAnyOrder(v1, v2); } - - private boolean denebMilestoneReached() { - return spec.getForkSchedule() - .getSpecMilestoneAtSlot(ONE) - .isGreaterThanOrEqualTo(SpecMilestone.DENEB); - } } diff --git a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java index 8f6ad8801bd..1f67e41c6a0 100644 --- a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java +++ b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java @@ -2159,11 +2159,15 @@ public BlockContents randomBlockContents() { } public BlindedBlockContents randomBlindedBlockContents() { + return randomBlindedBlockContents(randomUInt64()); + } + + public BlindedBlockContents randomBlindedBlockContents(UInt64 slot) { final List blindedBlobSidecarList = randomBlindedBlobSidecars(4); final BlindedBlobSidecars blindedBlobSidecars = new BlindedBlobSidecars( getSchemaDefinitionDeneb().getBlindedBlobSidecarsSchema(), blindedBlobSidecarList); - final BeaconBlock beaconBlock = randomBeaconBlock(); + final BeaconBlock beaconBlock = randomBeaconBlock(slot); return getSchemaDefinitionDeneb() .getBlindedBlockContentsSchema() .create(beaconBlock, blindedBlobSidecars); 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 20c723c6bba..3b3a630be35 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 @@ -283,7 +283,8 @@ public SafeFuture> createUnsignedBlock( @Override public SafeFuture> createUnsignedBlindedBlockContents( final UInt64 slot, final BLSSignature randaoReveal, Optional graffiti) { - throw new NotImplementedException("Not Yet Implemented"); + return sendRequest( + () -> typeDefClient.createUnsignedBlindedBlockContents(slot, randaoReveal, graffiti)); } @Override 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 f457ce83784..27bcf761eed 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 @@ -25,12 +25,14 @@ import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlock; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; +import tech.pegasys.teku.spec.datastructures.blocks.versions.deneb.BlindedBlockContents; import tech.pegasys.teku.spec.datastructures.builder.SignedValidatorRegistration; import tech.pegasys.teku.spec.datastructures.genesis.GenesisData; import tech.pegasys.teku.spec.datastructures.operations.AttestationData; import tech.pegasys.teku.validator.api.SendSignedBlockResult; import tech.pegasys.teku.validator.api.required.SyncingStatus; import tech.pegasys.teku.validator.remote.typedef.handlers.CreateAttestationDataRequest; +import tech.pegasys.teku.validator.remote.typedef.handlers.CreateBlindedBlockContentsRequest; import tech.pegasys.teku.validator.remote.typedef.handlers.CreateBlockRequest; import tech.pegasys.teku.validator.remote.typedef.handlers.GetGenesisRequest; import tech.pegasys.teku.validator.remote.typedef.handlers.GetSyncingStatusRequest; @@ -105,6 +107,15 @@ public Optional createUnsignedBlock( } } + public Optional createUnsignedBlindedBlockContents( + final UInt64 slot, final BLSSignature randaoReveal, final Optional graffiti) { + final CreateBlindedBlockContentsRequest createBlindedBlockContentsRequest = + new CreateBlindedBlockContentsRequest( + baseEndpoint, okHttpClient, spec, slot, preferSszBlockEncoding); + return createBlindedBlockContentsRequest.createUnsignedBlindedBlockContents( + randaoReveal, graffiti); + } + public void registerValidators( final SszList validatorRegistrations) { registerValidatorsRequest.registerValidators(validatorRegistrations); diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/CreateBlindedBlockContentsRequest.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/CreateBlindedBlockContentsRequest.java new file mode 100644 index 00000000000..2be0208a481 --- /dev/null +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/CreateBlindedBlockContentsRequest.java @@ -0,0 +1,152 @@ +/* + * 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 tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.validator.remote.apiclient.ValidatorApiMethod.GET_UNSIGNED_BLINDED_BLOCK; + +import com.google.common.net.MediaType; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import tech.pegasys.teku.bls.BLSSignature; +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.Spec; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.datastructures.blocks.versions.deneb.BlindedBlockContents; +import tech.pegasys.teku.spec.datastructures.blocks.versions.deneb.BlindedBlockContentsSchema; +import tech.pegasys.teku.validator.remote.typedef.ResponseHandler; + +public class CreateBlindedBlockContentsRequest extends AbstractTypeDefRequest { + + private static final Logger LOG = LogManager.getLogger(); + + private final UInt64 slot; + private final boolean preferSszBlockContentsEncoding; + private final BlindedBlockContentsSchema blindedBlockContentsSchema; + private final DeserializableTypeDefinition + getBlindedBlockContentsResponseDefinition; + private final ResponseHandler responseHandler; + + public CreateBlindedBlockContentsRequest( + final HttpUrl baseEndpoint, + final OkHttpClient okHttpClient, + final Spec spec, + final UInt64 slot, + final boolean preferSszBlockContentsEncoding) { + super(baseEndpoint, okHttpClient); + this.slot = slot; + this.preferSszBlockContentsEncoding = preferSszBlockContentsEncoding; + blindedBlockContentsSchema = + spec.atSlot(slot) + .getSchemaDefinitions() + .toVersionDeneb() + .orElseThrow() + .getBlindedBlockContentsSchema(); + getBlindedBlockContentsResponseDefinition = + DeserializableTypeDefinition.object(GetBlindedBlockContentsResponse.class) + .initializer(GetBlindedBlockContentsResponse::new) + .withField( + "data", + blindedBlockContentsSchema.getJsonTypeDefinition(), + GetBlindedBlockContentsResponse::getData, + GetBlindedBlockContentsResponse::setData) + .withField( + "version", + DeserializableTypeDefinition.enumOf(SpecMilestone.class), + GetBlindedBlockContentsResponse::getSpecMilestone, + GetBlindedBlockContentsResponse::setSpecMilestone) + .build(); + final ResponseHandler responseHandler = + new ResponseHandler<>(getBlindedBlockContentsResponseDefinition) + .withHandler(SC_OK, this::handleBlockContentsResult); + this.responseHandler = responseHandler; + } + + public Optional createUnsignedBlindedBlockContents( + final BLSSignature randaoReveal, final Optional graffiti) { + final Map queryParams = new HashMap<>(); + queryParams.put("randao_reveal", randaoReveal.toString()); + final Map headers = new HashMap<>(); + graffiti.ifPresent(bytes32 -> queryParams.put("graffiti", bytes32.toHexString())); + + if (preferSszBlockContentsEncoding) { + // application/octet-stream is preferred, but will accept application/json + headers.put("Accept", "application/octet-stream;q=0.9, application/json;q=0.4"); + } + return get( + GET_UNSIGNED_BLINDED_BLOCK, + Map.of("slot", slot.toString()), + queryParams, + headers, + responseHandler) + .map(GetBlindedBlockContentsResponse::getData); + } + + private Optional handleBlockContentsResult( + final Request request, final Response response) { + try { + final String responseContentType = response.header("Content-Type"); + if (responseContentType != null + && MediaType.parse(responseContentType).is(MediaType.OCTET_STREAM)) { + return Optional.of( + new GetBlindedBlockContentsResponse( + blindedBlockContentsSchema.sszDeserialize(Bytes.of(response.body().bytes())))); + } + return Optional.of( + JsonUtil.parse(response.body().string(), getBlindedBlockContentsResponseDefinition)); + } catch (IOException e) { + LOG.trace("Failed to parse response object creating block contents", e); + } + return Optional.empty(); + } + + public static class GetBlindedBlockContentsResponse { + private BlindedBlockContents data; + private SpecMilestone specMilestone; + + public GetBlindedBlockContentsResponse() {} + + public GetBlindedBlockContentsResponse(final BlindedBlockContents data) { + this.data = data; + } + + public BlindedBlockContents getData() { + return data; + } + + public void setData(final BlindedBlockContents data) { + this.data = data; + } + + public SpecMilestone getSpecMilestone() { + return specMilestone; + } + + public void setSpecMilestone(final SpecMilestone specMilestone) { + this.specMilestone = specMilestone; + } + } +} 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 55075d0866f..02e77b53c5c 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 @@ -66,6 +66,7 @@ import tech.pegasys.teku.spec.TestSpecFactory; import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlock; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; +import tech.pegasys.teku.spec.datastructures.blocks.versions.deneb.BlindedBlockContents; import tech.pegasys.teku.spec.datastructures.builder.SignedValidatorRegistration; import tech.pegasys.teku.spec.datastructures.genesis.GenesisData; import tech.pegasys.teku.spec.datastructures.operations.AggregateAndProof; @@ -409,6 +410,26 @@ public void createUnsignedBlock_WhenFound_ReturnsBlock() { assertThatSszData(unwrapToValue(future)).isEqualByAllMeansTo(beaconBlock); } + @Test + public void createUnsignedBlindedBlockContents_WhenFound_ReturnsBlockContents() { + final Spec denebSpec = TestSpecFactory.createMinimalDeneb(); + final DataStructureUtil denebDataStructureUtil = new DataStructureUtil(denebSpec); + final BeaconBlock beaconBlock = denebDataStructureUtil.randomBeaconBlock(UInt64.ONE); + final BlindedBlockContents blindedBlockContents = + denebDataStructureUtil.randomBlindedBlockContents(UInt64.ONE); + final BLSSignature blsSignature = denebDataStructureUtil.randomSignature(); + final Optional graffiti = Optional.of(Bytes32.random()); + + when(typeDefClient.createUnsignedBlindedBlockContents( + eq(beaconBlock.getSlot()), eq(blsSignature), eq(graffiti))) + .thenReturn(Optional.of(blindedBlockContents)); + + SafeFuture> future = + apiHandler.createUnsignedBlindedBlockContents(UInt64.ONE, blsSignature, graffiti); + + assertThatSszData(unwrapToValue(future)).isEqualByAllMeansTo(blindedBlockContents); + } + @Test public void sendSignedBlock_InvokeApiWithCorrectRequest() { final BeaconBlock beaconBlock = dataStructureUtil.randomBeaconBlock(UInt64.ONE);