diff --git a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/ExecutionEngineClient.java b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/ExecutionEngineClient.java index 19d8d8a3d97..0277f3de9c6 100644 --- a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/ExecutionEngineClient.java +++ b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/ExecutionEngineClient.java @@ -30,6 +30,7 @@ import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV1; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV2; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV3; +import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV4; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadStatusV1; import tech.pegasys.teku.ethereum.executionclient.schema.Response; import tech.pegasys.teku.infrastructure.async.SafeFuture; @@ -76,6 +77,9 @@ SafeFuture> forkChoiceUpdatedV2( SafeFuture> forkChoiceUpdatedV3( ForkChoiceStateV1 forkChoiceState, Optional payloadAttributes); + SafeFuture> forkChoiceUpdatedV4( + ForkChoiceStateV1 forkChoiceState, Optional payloadAttributes); + SafeFuture>> exchangeCapabilities(List capabilities); SafeFuture>> getClientVersionV1(ClientVersionV1 clientVersion); diff --git a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/ThrottlingExecutionEngineClient.java b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/ThrottlingExecutionEngineClient.java index 646a513c8dc..50e1a969a2e 100644 --- a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/ThrottlingExecutionEngineClient.java +++ b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/ThrottlingExecutionEngineClient.java @@ -31,6 +31,7 @@ import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV1; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV2; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV3; +import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV4; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadStatusV1; import tech.pegasys.teku.ethereum.executionclient.schema.Response; import tech.pegasys.teku.infrastructure.async.SafeFuture; @@ -144,6 +145,14 @@ public SafeFuture> forkChoiceUpdatedV3( () -> delegate.forkChoiceUpdatedV3(forkChoiceState, payloadAttributes)); } + @Override + public SafeFuture> forkChoiceUpdatedV4( + final ForkChoiceStateV1 forkChoiceState, + final Optional payloadAttributes) { + return taskQueue.queueTask( + () -> delegate.forkChoiceUpdatedV4(forkChoiceState, payloadAttributes)); + } + @Override public SafeFuture>> exchangeCapabilities(final List capabilities) { return taskQueue.queueTask(() -> delegate.exchangeCapabilities(capabilities)); diff --git a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineForkChoiceUpdatedV4.java b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineForkChoiceUpdatedV4.java new file mode 100644 index 00000000000..d00676e03dd --- /dev/null +++ b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineForkChoiceUpdatedV4.java @@ -0,0 +1,81 @@ +/* + * 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.ethereum.executionclient.methods; + +import java.util.Optional; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import tech.pegasys.teku.ethereum.executionclient.ExecutionEngineClient; +import tech.pegasys.teku.ethereum.executionclient.response.ResponseUnwrapper; +import tech.pegasys.teku.ethereum.executionclient.schema.ForkChoiceStateV1; +import tech.pegasys.teku.ethereum.executionclient.schema.ForkChoiceUpdatedResult; +import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV4; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.spec.executionlayer.ForkChoiceState; +import tech.pegasys.teku.spec.executionlayer.PayloadBuildingAttributes; + +public class EngineForkChoiceUpdatedV4 + extends AbstractEngineJsonRpcMethod< + tech.pegasys.teku.spec.executionlayer.ForkChoiceUpdatedResult> { + + private static final Logger LOG = LogManager.getLogger(); + + public EngineForkChoiceUpdatedV4(final ExecutionEngineClient executionEngineClient) { + super(executionEngineClient); + } + + @Override + public String getName() { + return EngineApiMethod.ENGINE_FORK_CHOICE_UPDATED.getName(); + } + + @Override + public int getVersion() { + return 4; + } + + @Override + public SafeFuture execute( + final JsonRpcRequestParams params) { + final ForkChoiceState forkChoiceState = params.getRequiredParameter(0, ForkChoiceState.class); + final Optional payloadBuildingAttributes = + params.getOptionalParameter(1, PayloadBuildingAttributes.class); + + LOG.trace( + "Calling {}(forkChoiceState={}, payloadAttributes={})", + getVersionedName(), + forkChoiceState, + payloadBuildingAttributes); + + final Optional maybePayloadAttributes = + payloadBuildingAttributes.flatMap( + attributes -> + PayloadAttributesV4.fromInternalPayloadBuildingAttributesV4( + payloadBuildingAttributes)); + + return executionEngineClient + .forkChoiceUpdatedV4( + ForkChoiceStateV1.fromInternalForkChoiceState(forkChoiceState), maybePayloadAttributes) + .thenApply(ResponseUnwrapper::unwrapExecutionClientResponseOrThrow) + .thenApply(ForkChoiceUpdatedResult::asInternalExecutionPayload) + .thenPeek( + forkChoiceUpdatedResult -> + LOG.trace( + "Response {}(forkChoiceState={}, payloadAttributes={}) -> {}", + getVersionedName(), + forkChoiceState, + payloadBuildingAttributes, + forkChoiceUpdatedResult)); + } +} diff --git a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/metrics/MetricRecordingExecutionEngineClient.java b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/metrics/MetricRecordingExecutionEngineClient.java index 62d3a4def02..ec4b9249a08 100644 --- a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/metrics/MetricRecordingExecutionEngineClient.java +++ b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/metrics/MetricRecordingExecutionEngineClient.java @@ -33,6 +33,7 @@ import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV1; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV2; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV3; +import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV4; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadStatusV1; import tech.pegasys.teku.ethereum.executionclient.schema.Response; import tech.pegasys.teku.infrastructure.async.SafeFuture; @@ -59,8 +60,11 @@ public class MetricRecordingExecutionEngineClient extends MetricRecordingAbstrac public static final String FORKCHOICE_UPDATED_WITH_ATTRIBUTES_V2_METHOD = "forkchoice_updated_with_attributesV2"; public static final String FORKCHOICE_UPDATED_V3_METHOD = "forkchoice_updatedV3"; + public static final String FORKCHOICE_UPDATED_V4_METHOD = "forkchoice_updatedV4"; public static final String FORKCHOICE_UPDATED_WITH_ATTRIBUTES_V3_METHOD = "forkchoice_updated_with_attributesV3"; + public static final String FORKCHOICE_UPDATED_WITH_ATTRIBUTES_V4_METHOD = + "forkchoice_updated_with_attributesV4"; public static final String GET_PAYLOAD_V3_METHOD = "get_payloadV3"; public static final String GET_PAYLOAD_V4_METHOD = "get_payloadV4"; public static final String NEW_PAYLOAD_V3_METHOD = "new_payloadV3"; @@ -185,6 +189,17 @@ public SafeFuture> forkChoiceUpdatedV3( : FORKCHOICE_UPDATED_V3_METHOD); } + @Override + public SafeFuture> forkChoiceUpdatedV4( + final ForkChoiceStateV1 forkChoiceState, + final Optional payloadAttributes) { + return countRequest( + () -> delegate.forkChoiceUpdatedV4(forkChoiceState, payloadAttributes), + payloadAttributes.isPresent() + ? FORKCHOICE_UPDATED_WITH_ATTRIBUTES_V4_METHOD + : FORKCHOICE_UPDATED_V4_METHOD); + } + @Override public SafeFuture>> exchangeCapabilities(final List capabilities) { return countRequest( diff --git a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/schema/PayloadAttributesV4.java b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/schema/PayloadAttributesV4.java new file mode 100644 index 00000000000..47e98eaad44 --- /dev/null +++ b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/schema/PayloadAttributesV4.java @@ -0,0 +1,115 @@ +/* + * Copyright Consensys Software Inc., 2022 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.ethereum.executionclient.schema; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.base.MoreObjects; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import org.apache.tuweni.bytes.Bytes32; +import tech.pegasys.teku.ethereum.executionclient.serialization.UInt64AsHexDeserializer; +import tech.pegasys.teku.ethereum.executionclient.serialization.UInt64AsHexSerializer; +import tech.pegasys.teku.infrastructure.bytes.Bytes20; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.executionlayer.PayloadBuildingAttributes; + +public class PayloadAttributesV4 extends PayloadAttributesV3 { + + @JsonSerialize(using = UInt64AsHexSerializer.class) + @JsonDeserialize(using = UInt64AsHexDeserializer.class) + public final UInt64 targetBlockCount; + + @JsonSerialize(using = UInt64AsHexSerializer.class) + @JsonDeserialize(using = UInt64AsHexDeserializer.class) + public final UInt64 maximumBlobCount; + + public PayloadAttributesV4( + final @JsonProperty("timestamp") UInt64 timestamp, + final @JsonProperty("prevRandao") Bytes32 prevRandao, + final @JsonProperty("suggestedFeeRecipient") Bytes20 suggestedFeeRecipient, + final @JsonProperty("withdrawals") List withdrawals, + final @JsonProperty("parentBeaconBlockRoot") Bytes32 parentBeaconBlockRoot, + final @JsonProperty("targetBlobCount") UInt64 targetBlockCount, + final @JsonProperty("maximumBlobCount") UInt64 maximumBlobCount) { + super(timestamp, prevRandao, suggestedFeeRecipient, withdrawals, parentBeaconBlockRoot); + + checkNotNull(targetBlockCount, "targetBlockCount"); + checkNotNull(maximumBlobCount, "maximumBlobCount"); + this.targetBlockCount = targetBlockCount; + this.maximumBlobCount = maximumBlobCount; + } + + public static Optional fromInternalPayloadBuildingAttributesV4( + final Optional payloadBuildingAttributes) { + return payloadBuildingAttributes.map( + payloadAttributes -> + new PayloadAttributesV4( + payloadAttributes.getTimestamp(), + payloadAttributes.getPrevRandao(), + payloadAttributes.getFeeRecipient(), + getWithdrawals(payloadAttributes), + payloadAttributes.getParentBeaconBlockRoot(), + payloadAttributes + .getTargetBlobCount() + .orElseThrow( + () -> + new IllegalArgumentException( + "targetBlobCount is required for PayloadAttributesV4")), + payloadAttributes + .getMaximumBlobCount() + .orElseThrow( + () -> + new IllegalArgumentException( + "maximumBlobCount is required for PayloadAttributesV4")))); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + final PayloadAttributesV4 that = (PayloadAttributesV4) o; + return Objects.equals(targetBlockCount, that.targetBlockCount) + && Objects.equals(maximumBlobCount, that.maximumBlobCount); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), targetBlockCount, maximumBlobCount); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("timestamp", timestamp) + .add("prevRandao", prevRandao) + .add("suggestedFeeRecipient", suggestedFeeRecipient) + .add("withdrawals", withdrawals) + .add("parentBeaconBlockRoot", parentBeaconBlockRoot) + .add("targetBlockCount", targetBlockCount) + .add("maximumBlobCount", maximumBlobCount) + .toString(); + } +} diff --git a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/web3j/Web3JExecutionEngineClient.java b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/web3j/Web3JExecutionEngineClient.java index 6eed83dd565..d029d8e4dfc 100644 --- a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/web3j/Web3JExecutionEngineClient.java +++ b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/web3j/Web3JExecutionEngineClient.java @@ -41,6 +41,7 @@ import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV1; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV2; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV3; +import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV4; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadStatusV1; import tech.pegasys.teku.ethereum.executionclient.schema.Response; import tech.pegasys.teku.infrastructure.async.SafeFuture; @@ -238,6 +239,19 @@ public SafeFuture> forkChoiceUpdatedV3( return web3JClient.doRequest(web3jRequest, EL_ENGINE_BLOCK_EXECUTION_TIMEOUT); } + @Override + public SafeFuture> forkChoiceUpdatedV4( + final ForkChoiceStateV1 forkChoiceState, + final Optional payloadAttributes) { + final Request web3jRequest = + new Request<>( + "engine_forkchoiceUpdatedV4", + list(forkChoiceState, payloadAttributes.orElse(null)), + web3JClient.getWeb3jService(), + ForkChoiceUpdatedResultWeb3jResponse.class); + return web3JClient.doRequest(web3jRequest, EL_ENGINE_BLOCK_EXECUTION_TIMEOUT); + } + @Override public SafeFuture>> exchangeCapabilities(final List capabilities) { final Request web3jRequest = diff --git a/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineForkChoiceUpdatedV4Test.java b/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineForkChoiceUpdatedV4Test.java new file mode 100644 index 00000000000..e666b69bb31 --- /dev/null +++ b/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineForkChoiceUpdatedV4Test.java @@ -0,0 +1,149 @@ +/* + * 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.ethereum.executionclient.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.ethereum.executionclient.ExecutionEngineClient; +import tech.pegasys.teku.ethereum.executionclient.response.InvalidRemoteResponseException; +import tech.pegasys.teku.ethereum.executionclient.schema.ForkChoiceStateV1; +import tech.pegasys.teku.ethereum.executionclient.schema.ForkChoiceUpdatedResult; +import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV4; +import tech.pegasys.teku.ethereum.executionclient.schema.PayloadStatusV1; +import tech.pegasys.teku.ethereum.executionclient.schema.Response; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.TestSpecFactory; +import tech.pegasys.teku.spec.executionlayer.ExecutionPayloadStatus; +import tech.pegasys.teku.spec.executionlayer.ForkChoiceState; +import tech.pegasys.teku.spec.executionlayer.PayloadBuildingAttributes; +import tech.pegasys.teku.spec.util.DataStructureUtil; + +class EngineForkChoiceUpdatedV4Test { + + private final Spec spec = TestSpecFactory.createMinimalDeneb(); + private final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec); + private final ExecutionEngineClient executionEngineClient = mock(ExecutionEngineClient.class); + private EngineForkChoiceUpdatedV4 jsonRpcMethod; + + @BeforeEach + public void setUp() { + jsonRpcMethod = new EngineForkChoiceUpdatedV4(executionEngineClient); + } + + @Test + public void shouldReturnExpectedNameAndVersion() { + assertThat(jsonRpcMethod.getName()).isEqualTo("engine_forkchoiceUpdated"); + assertThat(jsonRpcMethod.getVersion()).isEqualTo(4); + assertThat(jsonRpcMethod.getVersionedName()).isEqualTo("engine_forkchoiceUpdatedV4"); + } + + @Test + public void forkChoiceStateParamIsRequired() { + final JsonRpcRequestParams params = new JsonRpcRequestParams.Builder().build(); + + assertThatThrownBy(() -> jsonRpcMethod.execute(params)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Missing required parameter at index 0"); + + verifyNoInteractions(executionEngineClient); + } + + @Test + public void payloadBuildingAttributesParamIsOptional() { + final ForkChoiceState forkChoiceState = dataStructureUtil.randomForkChoiceState(false); + + when(executionEngineClient.forkChoiceUpdatedV4(any(), eq(Optional.empty()))) + .thenReturn(dummySuccessfulResponse()); + + final JsonRpcRequestParams params = + new JsonRpcRequestParams.Builder().add(forkChoiceState).build(); + + assertThat(jsonRpcMethod.execute(params)).isCompleted(); + + verify(executionEngineClient).forkChoiceUpdatedV4(any(), eq(Optional.empty())); + } + + @Test + public void shouldReturnFailedFutureWithMessageWhenEngineClientRequestFails() { + final ForkChoiceState forkChoiceState = dataStructureUtil.randomForkChoiceState(false); + final String errorResponseFromClient = "error!"; + + when(executionEngineClient.forkChoiceUpdatedV4(any(), any())) + .thenReturn(dummyFailedResponse(errorResponseFromClient)); + + final JsonRpcRequestParams params = + new JsonRpcRequestParams.Builder().add(forkChoiceState).build(); + + assertThat(jsonRpcMethod.execute(params)) + .failsWithin(1, TimeUnit.SECONDS) + .withThrowableOfType(ExecutionException.class) + .withRootCauseInstanceOf(InvalidRemoteResponseException.class) + .withMessageContaining( + "Invalid remote response from the execution client: %s", errorResponseFromClient); + } + + @Test + public void shouldCallForkChoiceUpdateV4WithPayloadAttributesV4WhenInElectra() { + final ForkChoiceState forkChoiceState = dataStructureUtil.randomForkChoiceState(false); + final PayloadBuildingAttributes payloadBuildingAttributes = + dataStructureUtil.randomPayloadBuildingAttributes(false); + final ForkChoiceStateV1 forkChoiceStateV1 = + ForkChoiceStateV1.fromInternalForkChoiceState(forkChoiceState); + final Optional payloadAttributesV4 = + PayloadAttributesV4.fromInternalPayloadBuildingAttributesV4( + Optional.of(payloadBuildingAttributes)); + + jsonRpcMethod = new EngineForkChoiceUpdatedV4(executionEngineClient); + + when(executionEngineClient.forkChoiceUpdatedV4(forkChoiceStateV1, payloadAttributesV4)) + .thenReturn(dummySuccessfulResponse()); + + final JsonRpcRequestParams params = + new JsonRpcRequestParams.Builder() + .add(forkChoiceState) + .add(payloadBuildingAttributes) + .build(); + + assertThat(jsonRpcMethod.execute(params)).isCompleted(); + + verify(executionEngineClient).forkChoiceUpdatedV4(forkChoiceStateV1, payloadAttributesV4); + } + + private SafeFuture> dummySuccessfulResponse() { + return SafeFuture.completedFuture( + new Response<>( + new ForkChoiceUpdatedResult( + new PayloadStatusV1( + ExecutionPayloadStatus.ACCEPTED, dataStructureUtil.randomBytes32(), ""), + dataStructureUtil.randomBytes8()))); + } + + private SafeFuture> dummyFailedResponse( + final String errorMessage) { + return SafeFuture.completedFuture(Response.withErrorMessage(errorMessage)); + } +} diff --git a/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/schema/PayloadAttributesV4Test.java b/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/schema/PayloadAttributesV4Test.java new file mode 100644 index 00000000000..0200500d25b --- /dev/null +++ b/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/schema/PayloadAttributesV4Test.java @@ -0,0 +1,100 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.ethereum.executionclient.schema; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Optional; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.TestSpecFactory; +import tech.pegasys.teku.spec.executionlayer.PayloadBuildingAttributes; +import tech.pegasys.teku.spec.util.DataStructureUtil; + +class PayloadAttributesV4Test { + + private final Spec spec = TestSpecFactory.createMinimalElectra(); + private final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec); + + @Test + public void buildFromInternalPayload_RequiresTargetBlobCount() { + final PayloadBuildingAttributes pbaMissingTargetBlobCount = + new PayloadBuildingAttributes( + dataStructureUtil.randomUInt64(), + dataStructureUtil.randomUInt64(), + dataStructureUtil.randomUInt64(), + dataStructureUtil.randomBytes32(), + dataStructureUtil.randomEth1Address(), + Optional.empty(), + Optional.empty(), + dataStructureUtil.randomBytes32(), + Optional.empty(), + Optional.of(dataStructureUtil.randomUInt64())); + + assertThrows( + IllegalArgumentException.class, + () -> + PayloadAttributesV4.fromInternalPayloadBuildingAttributesV4( + Optional.of(pbaMissingTargetBlobCount))); + } + + @Test + public void buildFromInternalPayload_RequiresMaximumBlobCount() { + final PayloadBuildingAttributes pbaMissingMaximumBlobCount = + new PayloadBuildingAttributes( + dataStructureUtil.randomUInt64(), + dataStructureUtil.randomUInt64(), + dataStructureUtil.randomUInt64(), + dataStructureUtil.randomBytes32(), + dataStructureUtil.randomEth1Address(), + Optional.empty(), + Optional.empty(), + dataStructureUtil.randomBytes32(), + Optional.of(dataStructureUtil.randomUInt64()), + Optional.empty()); + + assertThrows( + IllegalArgumentException.class, + () -> + PayloadAttributesV4.fromInternalPayloadBuildingAttributesV4( + Optional.of(pbaMissingMaximumBlobCount))); + } + + @Test + public void buildFromInternalPayload_HasCorrectValues() { + final PayloadBuildingAttributes payloadBuildingAttributes = + dataStructureUtil.randomPayloadBuildingAttributes(false); + + final PayloadAttributesV4 payloadAttributesV4 = + PayloadAttributesV4.fromInternalPayloadBuildingAttributesV4( + Optional.of(payloadBuildingAttributes)) + .orElseThrow(); + + assertThat(payloadBuildingAttributes.getTimestamp()).isEqualTo(payloadAttributesV4.timestamp); + assertThat(payloadBuildingAttributes.getPrevRandao()).isEqualTo(payloadAttributesV4.prevRandao); + assertThat(payloadBuildingAttributes.getFeeRecipient()) + .isEqualTo(payloadAttributesV4.suggestedFeeRecipient); + assertThat(payloadBuildingAttributes.getWithdrawals()) + .hasValueSatisfying( + withdrawals -> + assertEquals(withdrawals.size(), payloadAttributesV4.withdrawals.size())); + assertThat(payloadBuildingAttributes.getParentBeaconBlockRoot()) + .isEqualTo(payloadAttributesV4.parentBeaconBlockRoot); + assertThat(payloadBuildingAttributes.getTargetBlobCount()) + .hasValue(payloadAttributesV4.targetBlockCount); + assertThat(payloadBuildingAttributes.getMaximumBlobCount()) + .hasValue(payloadAttributesV4.maximumBlobCount); + } +} diff --git a/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/MilestoneBasedEngineJsonRpcMethodsResolver.java b/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/MilestoneBasedEngineJsonRpcMethodsResolver.java index 0651980f143..1635326d0f2 100644 --- a/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/MilestoneBasedEngineJsonRpcMethodsResolver.java +++ b/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/MilestoneBasedEngineJsonRpcMethodsResolver.java @@ -119,6 +119,8 @@ private Map> electraSupportedMethods() { methods.put(ENGINE_NEW_PAYLOAD, new EngineNewPayloadV4(executionEngineClient)); methods.put(ENGINE_GET_PAYLOAD, new EngineGetPayloadV4(executionEngineClient, spec)); + // TODO EIP-7742 Replace with EngineForkChoiceUpdatedV4 + // (https://github.com/Consensys/teku/issues/8745) methods.put(ENGINE_FORK_CHOICE_UPDATED, new EngineForkChoiceUpdatedV3(executionEngineClient)); methods.put(ENGINE_GET_BLOBS, new EngineGetBlobsV1(executionEngineClient, spec)); diff --git a/ethereum/executionlayer/src/test/java/tech/pegasys/teku/ethereum/executionlayer/ElectraExecutionClientHandlerTest.java b/ethereum/executionlayer/src/test/java/tech/pegasys/teku/ethereum/executionlayer/ElectraExecutionClientHandlerTest.java index 1665419dfba..35a311f56f0 100644 --- a/ethereum/executionlayer/src/test/java/tech/pegasys/teku/ethereum/executionlayer/ElectraExecutionClientHandlerTest.java +++ b/ethereum/executionlayer/src/test/java/tech/pegasys/teku/ethereum/executionlayer/ElectraExecutionClientHandlerTest.java @@ -128,6 +128,7 @@ void engineNewPayload_shouldCallNewPayloadV4() { @Test void engineForkChoiceUpdated_shouldCallEngineForkChoiceUpdatedV3() { + // TODO EIP-7742 should call FcUV4 (https://github.com/Consensys/teku/issues/8745) final ExecutionClientHandler handler = getHandler(); final ForkChoiceState forkChoiceState = dataStructureUtil.randomForkChoiceState(false); final ForkChoiceStateV1 forkChoiceStateV1 = diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/executionlayer/PayloadBuildingAttributes.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/executionlayer/PayloadBuildingAttributes.java index 30a505f1d6e..7ec469600b8 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/executionlayer/PayloadBuildingAttributes.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/executionlayer/PayloadBuildingAttributes.java @@ -34,6 +34,8 @@ public class PayloadBuildingAttributes { private final Optional validatorRegistration; private final Optional> withdrawals; private final Bytes32 parentBeaconBlockRoot; + private final Optional targetBlobCount; + private final Optional maximumBlobCount; public PayloadBuildingAttributes( final UInt64 proposerIndex, @@ -44,6 +46,30 @@ public PayloadBuildingAttributes( final Optional validatorRegistration, final Optional> withdrawals, final Bytes32 parentBeaconBlockRoot) { + this( + proposerIndex, + proposalSlot, + timestamp, + prevRandao, + feeRecipient, + validatorRegistration, + withdrawals, + parentBeaconBlockRoot, + Optional.empty(), + Optional.empty()); + } + + public PayloadBuildingAttributes( + final UInt64 proposerIndex, + final UInt64 proposalSlot, + final UInt64 timestamp, + final Bytes32 prevRandao, + final Eth1Address feeRecipient, + final Optional validatorRegistration, + final Optional> withdrawals, + final Bytes32 parentBeaconBlockRoot, + final Optional targetBlobCount, + final Optional maximumBlobCount) { this.proposerIndex = proposerIndex; this.proposalSlot = proposalSlot; this.timestamp = timestamp; @@ -52,6 +78,8 @@ public PayloadBuildingAttributes( this.validatorRegistration = validatorRegistration; this.withdrawals = withdrawals; this.parentBeaconBlockRoot = parentBeaconBlockRoot; + this.targetBlobCount = targetBlobCount; + this.maximumBlobCount = maximumBlobCount; } public UInt64 getProposerIndex() { @@ -78,6 +106,14 @@ public Bytes32 getParentBeaconBlockRoot() { return parentBeaconBlockRoot; } + public Optional getTargetBlobCount() { + return targetBlobCount; + } + + public Optional getMaximumBlobCount() { + return maximumBlobCount; + } + public Optional getValidatorRegistration() { return validatorRegistration; } @@ -107,7 +143,9 @@ public boolean equals(final Object o) { && Objects.equals(feeRecipient, that.feeRecipient) && Objects.equals(validatorRegistration, that.validatorRegistration) && Objects.equals(withdrawals, that.withdrawals) - && Objects.equals(parentBeaconBlockRoot, that.parentBeaconBlockRoot); + && Objects.equals(parentBeaconBlockRoot, that.parentBeaconBlockRoot) + && Objects.equals(targetBlobCount, that.targetBlobCount) + && Objects.equals(maximumBlobCount, that.maximumBlobCount); } @Override @@ -120,7 +158,9 @@ public int hashCode() { feeRecipient, validatorRegistration, withdrawals, - parentBeaconBlockRoot); + parentBeaconBlockRoot, + targetBlobCount, + maximumBlobCount); } @Override @@ -134,6 +174,8 @@ public String toString() { .add("validatorRegistration", validatorRegistration) .add("withdrawals", withdrawals) .add("parentBeaconBlockRoot", parentBeaconBlockRoot) + .add("targetBlobCount", targetBlobCount) + .add("maximumBlobCount", maximumBlobCount) .toString(); } } 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 55ca1200742..b4aae61c5df 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 @@ -1783,7 +1783,9 @@ public PayloadBuildingAttributes randomPayloadBuildingAttributes( ? Optional.of(randomSignedValidatorRegistration()) : Optional.empty(), randomWithdrawalList(), - randomBytes32()); + randomBytes32(), + Optional.of(randomUInt64()), + Optional.of(randomUInt64())); } public ClientVersion randomClientVersion() { diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ProposersDataManager.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ProposersDataManager.java index d32f9e6fed3..5f8f459a3cf 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ProposersDataManager.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ProposersDataManager.java @@ -238,6 +238,8 @@ private Optional calculatePayloadBuildingAttributes( final Eth1Address feeRecipient = getFeeRecipient(proposerInfo, blockSlot); + // TODO EIP-7742 add targetBlobCount and maximumBlobCount + // (https://github.com/Consensys/teku/issues/8745) return Optional.of( new PayloadBuildingAttributes( proposerIndex, @@ -247,7 +249,9 @@ private Optional calculatePayloadBuildingAttributes( feeRecipient, validatorRegistration, spec.getExpectedWithdrawals(state), - currentHeadBlockRoot)); + currentHeadBlockRoot, + Optional.empty(), + Optional.empty())); } // this function MUST return a fee recipient.