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 c4e94d8b45e..02ee14282b5 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 @@ -20,10 +20,12 @@ import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV1; import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV2; import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV3; +import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV4; import tech.pegasys.teku.ethereum.executionclient.schema.ForkChoiceStateV1; import tech.pegasys.teku.ethereum.executionclient.schema.ForkChoiceUpdatedResult; import tech.pegasys.teku.ethereum.executionclient.schema.GetPayloadV2Response; import tech.pegasys.teku.ethereum.executionclient.schema.GetPayloadV3Response; +import tech.pegasys.teku.ethereum.executionclient.schema.GetPayloadV4Response; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV1; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV2; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV3; @@ -47,6 +49,8 @@ public interface ExecutionEngineClient { SafeFuture> getPayloadV3(Bytes8 payloadId); + SafeFuture> getPayloadV4(Bytes8 payloadId); + SafeFuture> newPayloadV1(ExecutionPayloadV1 executionPayload); SafeFuture> newPayloadV2(ExecutionPayloadV2 executionPayload); @@ -56,6 +60,11 @@ SafeFuture> newPayloadV3( List blobVersionedHashes, Bytes32 parentBeaconBlockRoot); + SafeFuture> newPayloadV4( + ExecutionPayloadV4 executionPayload, + List blobVersionedHashes, + Bytes32 parentBeaconBlockRoot); + SafeFuture> forkChoiceUpdatedV1( ForkChoiceStateV1 forkChoiceState, Optional payloadAttributes); 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 45bb127707a..964c527cd4b 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 @@ -21,10 +21,12 @@ import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV1; import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV2; import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV3; +import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV4; import tech.pegasys.teku.ethereum.executionclient.schema.ForkChoiceStateV1; import tech.pegasys.teku.ethereum.executionclient.schema.ForkChoiceUpdatedResult; import tech.pegasys.teku.ethereum.executionclient.schema.GetPayloadV2Response; import tech.pegasys.teku.ethereum.executionclient.schema.GetPayloadV3Response; +import tech.pegasys.teku.ethereum.executionclient.schema.GetPayloadV4Response; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV1; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV2; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV3; @@ -79,6 +81,11 @@ public SafeFuture> getPayloadV3(final Bytes8 payl return taskQueue.queueTask(() -> delegate.getPayloadV3(payloadId)); } + @Override + public SafeFuture> getPayloadV4(final Bytes8 payloadId) { + return taskQueue.queueTask(() -> delegate.getPayloadV4(payloadId)); + } + @Override public SafeFuture> newPayloadV1( final ExecutionPayloadV1 executionPayload) { @@ -100,6 +107,15 @@ public SafeFuture> newPayloadV3( () -> delegate.newPayloadV3(executionPayload, blobVersionedHashes, parentBeaconBlockRoot)); } + @Override + public SafeFuture> newPayloadV4( + final ExecutionPayloadV4 executionPayload, + final List blobVersionedHashes, + final Bytes32 parentBeaconBlockRoot) { + return taskQueue.queueTask( + () -> delegate.newPayloadV4(executionPayload, blobVersionedHashes, parentBeaconBlockRoot)); + } + @Override public SafeFuture> forkChoiceUpdatedV1( final ForkChoiceStateV1 forkChoiceState, diff --git a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineGetPayloadV4.java b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineGetPayloadV4.java new file mode 100644 index 00000000000..cedf1fbe2aa --- /dev/null +++ b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineGetPayloadV4.java @@ -0,0 +1,101 @@ +/* + * 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 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.GetPayloadV4Response; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSchema; +import tech.pegasys.teku.spec.datastructures.execution.BlobsBundle; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayload; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadContext; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadSchema; +import tech.pegasys.teku.spec.datastructures.execution.GetPayloadResponse; +import tech.pegasys.teku.spec.schemas.SchemaDefinitions; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsBellatrix; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsDeneb; + +public class EngineGetPayloadV4 extends AbstractEngineJsonRpcMethod { + + private static final Logger LOG = LogManager.getLogger(); + + private final Spec spec; + + public EngineGetPayloadV4(final ExecutionEngineClient executionEngineClient, final Spec spec) { + super(executionEngineClient); + this.spec = spec; + } + + @Override + public String getName() { + return EngineApiMethod.ENGINE_GET_PAYLOAD.getName(); + } + + @Override + public int getVersion() { + return 4; + } + + @Override + public SafeFuture execute(final JsonRpcRequestParams params) { + final ExecutionPayloadContext executionPayloadContext = + params.getRequiredParameter(0, ExecutionPayloadContext.class); + final UInt64 slot = params.getRequiredParameter(1, UInt64.class); + + LOG.trace( + "Calling {}(payloadId={}, slot={})", + getVersionedName(), + executionPayloadContext.getPayloadId(), + slot); + + return executionEngineClient + .getPayloadV4(executionPayloadContext.getPayloadId()) + .thenApply(ResponseUnwrapper::unwrapExecutionClientResponseOrThrow) + .thenApply( + response -> { + final SchemaDefinitions schemaDefinitions = spec.atSlot(slot).getSchemaDefinitions(); + final ExecutionPayloadSchema payloadSchema = + SchemaDefinitionsBellatrix.required(schemaDefinitions) + .getExecutionPayloadSchema(); + final ExecutionPayload executionPayload = + response.executionPayload.asInternalExecutionPayload(payloadSchema); + final BlobsBundle blobsBundle = getBlobsBundle(response, schemaDefinitions); + return new GetPayloadResponse( + executionPayload, + response.blockValue, + blobsBundle, + response.shouldOverrideBuilder); + }) + .thenPeek( + getPayloadResponse -> + LOG.trace( + "Response {}(payloadId={}, slot={}) -> {}", + getVersionedName(), + executionPayloadContext.getPayloadId(), + slot, + getPayloadResponse)); + } + + private BlobsBundle getBlobsBundle( + final GetPayloadV4Response response, final SchemaDefinitions schemaDefinitions) { + final BlobSchema blobSchema = + SchemaDefinitionsDeneb.required(schemaDefinitions).getBlobSchema(); + return response.blobsBundle.asInternalBlobsBundle(blobSchema); + } +} diff --git a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineNewPayloadV4.java b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineNewPayloadV4.java new file mode 100644 index 00000000000..250ddfcc8f9 --- /dev/null +++ b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineNewPayloadV4.java @@ -0,0 +1,77 @@ +/* + * 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.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes32; +import tech.pegasys.teku.ethereum.executionclient.ExecutionEngineClient; +import tech.pegasys.teku.ethereum.executionclient.response.ResponseUnwrapper; +import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV4; +import tech.pegasys.teku.ethereum.executionclient.schema.PayloadStatusV1; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayload; +import tech.pegasys.teku.spec.executionlayer.PayloadStatus; +import tech.pegasys.teku.spec.logic.versions.deneb.types.VersionedHash; + +public class EngineNewPayloadV4 extends AbstractEngineJsonRpcMethod { + + private static final Logger LOG = LogManager.getLogger(); + + public EngineNewPayloadV4(final ExecutionEngineClient executionEngineClient) { + super(executionEngineClient); + } + + @Override + public String getName() { + return EngineApiMethod.ENGINE_NEW_PAYLOAD.getName(); + } + + @Override + public int getVersion() { + return 4; + } + + @Override + public SafeFuture execute(final JsonRpcRequestParams params) { + final ExecutionPayload executionPayload = + params.getRequiredParameter(0, ExecutionPayload.class); + final List blobVersionedHashes = + params.getRequiredListParameter(1, VersionedHash.class); + final Bytes32 parentBeaconBlockRoot = params.getRequiredParameter(2, Bytes32.class); + + LOG.trace( + "Calling {}(executionPayload={}, blobVersionedHashes={}, parentBeaconBlockRoot={})", + getVersionedName(), + executionPayload, + blobVersionedHashes, + parentBeaconBlockRoot); + + final ExecutionPayloadV4 executionPayloadV4 = + ExecutionPayloadV4.fromInternalExecutionPayload(executionPayload); + return executionEngineClient + .newPayloadV4(executionPayloadV4, blobVersionedHashes, parentBeaconBlockRoot) + .thenApply(ResponseUnwrapper::unwrapExecutionClientResponseOrThrow) + .thenApply(PayloadStatusV1::asInternalExecutionPayload) + .thenPeek( + payloadStatus -> + LOG.trace( + "Response {}(executionPayload={}) -> {}", + getVersionedName(), + executionPayload, + payloadStatus)) + .exceptionally(PayloadStatus::failedExecution); + } +} 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 6a4815b58c8..f45056c92a0 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 @@ -23,10 +23,12 @@ import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV1; import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV2; import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV3; +import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV4; import tech.pegasys.teku.ethereum.executionclient.schema.ForkChoiceStateV1; import tech.pegasys.teku.ethereum.executionclient.schema.ForkChoiceUpdatedResult; import tech.pegasys.teku.ethereum.executionclient.schema.GetPayloadV2Response; import tech.pegasys.teku.ethereum.executionclient.schema.GetPayloadV3Response; +import tech.pegasys.teku.ethereum.executionclient.schema.GetPayloadV4Response; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV1; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV2; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV3; @@ -59,7 +61,9 @@ public class MetricRecordingExecutionEngineClient extends MetricRecordingAbstrac public static final String FORKCHOICE_UPDATED_WITH_ATTRIBUTES_V3_METHOD = "forkchoice_updated_with_attributesV3"; 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"; + public static final String NEW_PAYLOAD_V4_METHOD = "new_payloadV4"; public static final String EXCHANGE_CAPABILITIES_METHOD = "exchange_capabilities"; public static final String GET_CLIENT_VERSION_V1_METHOD = "get_client_versionV1"; @@ -106,6 +110,11 @@ public SafeFuture> getPayloadV3(final Bytes8 payl return countRequest(() -> delegate.getPayloadV3(payloadId), GET_PAYLOAD_V3_METHOD); } + @Override + public SafeFuture> getPayloadV4(final Bytes8 payloadId) { + return countRequest(() -> delegate.getPayloadV4(payloadId), GET_PAYLOAD_V4_METHOD); + } + @Override public SafeFuture> newPayloadV1( final ExecutionPayloadV1 executionPayload) { @@ -128,6 +137,16 @@ public SafeFuture> newPayloadV3( NEW_PAYLOAD_V3_METHOD); } + @Override + public SafeFuture> newPayloadV4( + final ExecutionPayloadV4 executionPayload, + final List blobVersionedHashes, + final Bytes32 parentBeaconBlockRoot) { + return countRequest( + () -> delegate.newPayloadV4(executionPayload, blobVersionedHashes, parentBeaconBlockRoot), + NEW_PAYLOAD_V4_METHOD); + } + @Override public SafeFuture> forkChoiceUpdatedV1( final ForkChoiceStateV1 forkChoiceState, diff --git a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/schema/DepositReceiptV1.java b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/schema/DepositReceiptV1.java new file mode 100644 index 00000000000..7d2b70f0ac8 --- /dev/null +++ b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/schema/DepositReceiptV1.java @@ -0,0 +1,63 @@ +/* + * 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 com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.bytes.Bytes48; +import tech.pegasys.teku.ethereum.executionclient.serialization.Bytes32Deserializer; +import tech.pegasys.teku.ethereum.executionclient.serialization.Bytes48Deserializer; +import tech.pegasys.teku.ethereum.executionclient.serialization.BytesDeserializer; +import tech.pegasys.teku.ethereum.executionclient.serialization.BytesSerializer; +import tech.pegasys.teku.ethereum.executionclient.serialization.UInt64AsHexDeserializer; +import tech.pegasys.teku.ethereum.executionclient.serialization.UInt64AsHexSerializer; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; + +public class DepositReceiptV1 { + @JsonSerialize(using = BytesSerializer.class) + @JsonDeserialize(using = Bytes48Deserializer.class) + public final Bytes48 pubkey; + + @JsonSerialize(using = BytesSerializer.class) + @JsonDeserialize(using = Bytes32Deserializer.class) + public final Bytes32 withdrawalCredentials; + + @JsonSerialize(using = UInt64AsHexSerializer.class) + @JsonDeserialize(using = UInt64AsHexDeserializer.class) + public final UInt64 amount; + + @JsonSerialize(using = BytesSerializer.class) + @JsonDeserialize(using = BytesDeserializer.class) + public final Bytes signature; + + @JsonSerialize(using = UInt64AsHexSerializer.class) + @JsonDeserialize(using = UInt64AsHexDeserializer.class) + public final UInt64 index; + + public DepositReceiptV1( + @JsonProperty("pubkey") final Bytes48 pubkey, + @JsonProperty("withdrawalCredentials") final Bytes32 withdrawalCredentials, + @JsonProperty("amount") final UInt64 amount, + @JsonProperty("signature") final Bytes signature, + @JsonProperty("index") final UInt64 index) { + this.pubkey = pubkey; + this.withdrawalCredentials = withdrawalCredentials; + this.amount = amount; + this.signature = signature; + this.index = index; + } +} diff --git a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/schema/ExecutionPayloadV4.java b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/schema/ExecutionPayloadV4.java new file mode 100644 index 00000000000..9b14eb870ab --- /dev/null +++ b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/schema/ExecutionPayloadV4.java @@ -0,0 +1,184 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt256; +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.bls.BLSSignature; +import tech.pegasys.teku.infrastructure.bytes.Bytes20; +import tech.pegasys.teku.infrastructure.ssz.SszList; +import tech.pegasys.teku.infrastructure.ssz.collections.impl.SszByteListImpl; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayload; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadBuilder; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadSchema; +import tech.pegasys.teku.spec.datastructures.execution.versions.deneb.ExecutionPayloadDeneb; +import tech.pegasys.teku.spec.datastructures.execution.versions.electra.DepositReceipt; +import tech.pegasys.teku.spec.datastructures.execution.versions.electra.ExecutionLayerExit; +import tech.pegasys.teku.spec.datastructures.execution.versions.electra.ExecutionPayloadElectra; + +public class ExecutionPayloadV4 extends ExecutionPayloadV3 { + public final List depositReceipts; + public final List exits; + + public ExecutionPayloadV4( + @JsonProperty("parentHash") Bytes32 parentHash, + @JsonProperty("feeRecipient") Bytes20 feeRecipient, + @JsonProperty("stateRoot") Bytes32 stateRoot, + @JsonProperty("receiptsRoot") Bytes32 receiptsRoot, + @JsonProperty("logsBloom") Bytes logsBloom, + @JsonProperty("prevRandao") Bytes32 prevRandao, + @JsonProperty("blockNumber") UInt64 blockNumber, + @JsonProperty("gasLimit") UInt64 gasLimit, + @JsonProperty("gasUsed") UInt64 gasUsed, + @JsonProperty("timestamp") UInt64 timestamp, + @JsonProperty("extraData") Bytes extraData, + @JsonProperty("baseFeePerGas") UInt256 baseFeePerGas, + @JsonProperty("blockHash") Bytes32 blockHash, + @JsonProperty("transactions") List transactions, + @JsonProperty("withdrawals") List withdrawals, + @JsonProperty("blobGasUsed") UInt64 blobGasUsed, + @JsonProperty("excessBlobGas") UInt64 excessBlobGas, + @JsonProperty("depositReceipts") List depositReceipts, + @JsonProperty("exits") List exits) { + super( + parentHash, + feeRecipient, + stateRoot, + receiptsRoot, + logsBloom, + prevRandao, + blockNumber, + gasLimit, + gasUsed, + timestamp, + extraData, + baseFeePerGas, + blockHash, + transactions, + withdrawals, + blobGasUsed, + excessBlobGas); + this.depositReceipts = depositReceipts; + this.exits = exits; + } + + public static ExecutionPayloadV4 fromInternalExecutionPayload( + final ExecutionPayload executionPayload) { + final List withdrawalsList = + getWithdrawals(executionPayload.getOptionalWithdrawals()); + return new ExecutionPayloadV4( + executionPayload.getParentHash(), + executionPayload.getFeeRecipient(), + executionPayload.getStateRoot(), + executionPayload.getReceiptsRoot(), + executionPayload.getLogsBloom(), + executionPayload.getPrevRandao(), + executionPayload.getBlockNumber(), + executionPayload.getGasLimit(), + executionPayload.getGasUsed(), + executionPayload.getTimestamp(), + executionPayload.getExtraData(), + executionPayload.getBaseFeePerGas(), + executionPayload.getBlockHash(), + executionPayload.getTransactions().stream().map(SszByteListImpl::getBytes).toList(), + withdrawalsList, + executionPayload.toVersionDeneb().map(ExecutionPayloadDeneb::getBlobGasUsed).orElse(null), + executionPayload.toVersionDeneb().map(ExecutionPayloadDeneb::getExcessBlobGas).orElse(null), + getDepositReceipts( + executionPayload.toVersionElectra().map(ExecutionPayloadElectra::getDepositReceipts)), + getExits(executionPayload.toVersionElectra().map(ExecutionPayloadElectra::getExits))); + } + + @Override + protected ExecutionPayloadBuilder applyToBuilder( + final ExecutionPayloadSchema executionPayloadSchema, + final ExecutionPayloadBuilder builder) { + return super.applyToBuilder(executionPayloadSchema, builder) + .depositReceipts( + () -> + checkNotNull(depositReceipts, "depositReceipts not provided when required").stream() + .map( + depositReceiptV1 -> + createInternalDepositReceipt(depositReceiptV1, executionPayloadSchema)) + .toList()) + .exits( + () -> + checkNotNull(exits, "exits not provided when required").stream() + .map(exitV1 -> createInternalExit(exitV1, executionPayloadSchema)) + .toList()); + } + + private DepositReceipt createInternalDepositReceipt( + final DepositReceiptV1 depositReceiptV1, + final ExecutionPayloadSchema executionPayloadSchema) { + return executionPayloadSchema + .getDepositReceiptSchemaRequired() + .create( + BLSPublicKey.fromBytesCompressed(depositReceiptV1.pubkey), + depositReceiptV1.withdrawalCredentials, + depositReceiptV1.amount, + BLSSignature.fromBytesCompressed(depositReceiptV1.signature), + depositReceiptV1.index); + } + + private ExecutionLayerExit createInternalExit( + final ExitV1 exitV1, final ExecutionPayloadSchema executionPayloadSchema) { + return executionPayloadSchema + .getExecutionLayerExitSchemaRequired() + .create(exitV1.sourceAddress, BLSPublicKey.fromBytesCompressed(exitV1.validatorPublicKey)); + } + + public static List getDepositReceipts( + final Optional> maybeDepositReceipts) { + if (maybeDepositReceipts.isEmpty()) { + return List.of(); + } + + final List depositReceipts = new ArrayList<>(); + + for (DepositReceipt depositReceipt : maybeDepositReceipts.get()) { + depositReceipts.add( + new DepositReceiptV1( + depositReceipt.getPubkey().toBytesCompressed(), + depositReceipt.getWithdrawalCredentials(), + depositReceipt.getAmount(), + depositReceipt.getSignature().toBytesCompressed(), + depositReceipt.getIndex())); + } + return depositReceipts; + } + + public static List getExits(final Optional> maybeExits) { + if (maybeExits.isEmpty()) { + return List.of(); + } + + final List exits = new ArrayList<>(); + + for (ExecutionLayerExit exit : maybeExits.get()) { + exits.add( + new ExitV1(exit.getSourceAddress(), exit.getValidatorPublicKey().toBytesCompressed())); + } + return exits; + } +} diff --git a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/schema/ExitV1.java b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/schema/ExitV1.java new file mode 100644 index 00000000000..aa8bede2a75 --- /dev/null +++ b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/schema/ExitV1.java @@ -0,0 +1,41 @@ +/* + * 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 com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.apache.tuweni.bytes.Bytes48; +import tech.pegasys.teku.ethereum.executionclient.serialization.Bytes20Deserializer; +import tech.pegasys.teku.ethereum.executionclient.serialization.Bytes20Serializer; +import tech.pegasys.teku.ethereum.executionclient.serialization.Bytes48Deserializer; +import tech.pegasys.teku.ethereum.executionclient.serialization.BytesSerializer; +import tech.pegasys.teku.infrastructure.bytes.Bytes20; + +public class ExitV1 { + @JsonSerialize(using = Bytes20Serializer.class) + @JsonDeserialize(using = Bytes20Deserializer.class) + public final Bytes20 sourceAddress; + + @JsonSerialize(using = BytesSerializer.class) + @JsonDeserialize(using = Bytes48Deserializer.class) + public final Bytes48 validatorPublicKey; + + public ExitV1( + @JsonProperty("sourceAddress") final Bytes20 sourceAddress, + @JsonProperty("validatorPublicKey") final Bytes48 validatorPublicKey) { + this.sourceAddress = sourceAddress; + this.validatorPublicKey = validatorPublicKey; + } +} diff --git a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/schema/GetPayloadV4Response.java b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/schema/GetPayloadV4Response.java new file mode 100644 index 00000000000..09d959dbe47 --- /dev/null +++ b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/schema/GetPayloadV4Response.java @@ -0,0 +1,44 @@ +/* + * 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 com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.apache.tuweni.units.bigints.UInt256; +import tech.pegasys.teku.ethereum.executionclient.serialization.UInt256AsHexDeserializer; +import tech.pegasys.teku.ethereum.executionclient.serialization.UInt256AsHexSerializer; + +public class GetPayloadV4Response { + public final ExecutionPayloadV4 executionPayload; + + @JsonSerialize(using = UInt256AsHexSerializer.class) + @JsonDeserialize(using = UInt256AsHexDeserializer.class) + public final UInt256 blockValue; + + public final BlobsBundleV1 blobsBundle; + + public final boolean shouldOverrideBuilder; + + public GetPayloadV4Response( + @JsonProperty("executionPayload") final ExecutionPayloadV4 executionPayload, + @JsonProperty("blockValue") final UInt256 blockValue, + @JsonProperty("blobsBundle") final BlobsBundleV1 blobsBundle, + @JsonProperty("shouldOverrideBuilder") final boolean shouldOverrideBuilder) { + this.executionPayload = executionPayload; + this.blockValue = blockValue; + this.blobsBundle = blobsBundle; + this.shouldOverrideBuilder = shouldOverrideBuilder; + } +} 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 633cf0c55a6..d27e5307b04 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 @@ -31,10 +31,12 @@ import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV1; import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV2; import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV3; +import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV4; import tech.pegasys.teku.ethereum.executionclient.schema.ForkChoiceStateV1; import tech.pegasys.teku.ethereum.executionclient.schema.ForkChoiceUpdatedResult; import tech.pegasys.teku.ethereum.executionclient.schema.GetPayloadV2Response; import tech.pegasys.teku.ethereum.executionclient.schema.GetPayloadV3Response; +import tech.pegasys.teku.ethereum.executionclient.schema.GetPayloadV4Response; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV1; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV2; import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV3; @@ -120,6 +122,17 @@ public SafeFuture> getPayloadV3(final Bytes8 payl return web3JClient.doRequest(web3jRequest, EL_ENGINE_NON_BLOCK_EXECUTION_TIMEOUT); } + @Override + public SafeFuture> getPayloadV4(final Bytes8 payloadId) { + final Request web3jRequest = + new Request<>( + "engine_getPayloadV4", + Collections.singletonList(payloadId.toHexString()), + web3JClient.getWeb3jService(), + GetPayloadV4Web3jResponse.class); + return web3JClient.doRequest(web3jRequest, EL_ENGINE_NON_BLOCK_EXECUTION_TIMEOUT); + } + @Override public SafeFuture> newPayloadV1(ExecutionPayloadV1 executionPayload) { final Request web3jRequest = @@ -160,6 +173,23 @@ public SafeFuture> newPayloadV3( return web3JClient.doRequest(web3jRequest, EL_ENGINE_BLOCK_EXECUTION_TIMEOUT); } + @Override + public SafeFuture> newPayloadV4( + final ExecutionPayloadV4 executionPayload, + final List blobVersionedHashes, + final Bytes32 parentBeaconBlockRoot) { + final List expectedBlobVersionedHashes = + blobVersionedHashes.stream().map(VersionedHash::toHexString).toList(); + final Request web3jRequest = + new Request<>( + "engine_newPayloadV4", + list( + executionPayload, expectedBlobVersionedHashes, parentBeaconBlockRoot.toHexString()), + web3JClient.getWeb3jService(), + PayloadStatusV1Web3jResponse.class); + return web3JClient.doRequest(web3jRequest, EL_ENGINE_BLOCK_EXECUTION_TIMEOUT); + } + @Override public SafeFuture> forkChoiceUpdatedV1( ForkChoiceStateV1 forkChoiceState, Optional payloadAttributes) { @@ -230,6 +260,9 @@ static class GetPayloadV2Web3jResponse static class GetPayloadV3Web3jResponse extends org.web3j.protocol.core.Response {} + static class GetPayloadV4Web3jResponse + extends org.web3j.protocol.core.Response {} + static class PayloadStatusV1Web3jResponse extends org.web3j.protocol.core.Response {} diff --git a/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineGetPayloadV4Test.java b/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineGetPayloadV4Test.java new file mode 100644 index 00000000000..8ae7b63e7b3 --- /dev/null +++ b/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineGetPayloadV4Test.java @@ -0,0 +1,155 @@ +/* + * 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.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.apache.tuweni.units.bigints.UInt256; +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.BlobsBundleV1; +import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV4; +import tech.pegasys.teku.ethereum.executionclient.schema.GetPayloadV4Response; +import tech.pegasys.teku.ethereum.executionclient.schema.Response; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.TestSpecFactory; +import tech.pegasys.teku.spec.datastructures.execution.BlobsBundle; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayload; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadContext; +import tech.pegasys.teku.spec.datastructures.execution.GetPayloadResponse; +import tech.pegasys.teku.spec.datastructures.execution.versions.electra.ExecutionPayloadElectra; +import tech.pegasys.teku.spec.util.DataStructureUtil; + +class EngineGetPayloadV4Test { + + private final Spec spec = TestSpecFactory.createMinimalElectra(); + private final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec); + private final ExecutionEngineClient executionEngineClient = mock(ExecutionEngineClient.class); + private EngineGetPayloadV4 jsonRpcMethod; + + @BeforeEach + public void setUp() { + jsonRpcMethod = new EngineGetPayloadV4(executionEngineClient, spec); + } + + @Test + public void shouldReturnExpectedNameAndVersion() { + assertThat(jsonRpcMethod.getName()).isEqualTo("engine_getPayload"); + assertThat(jsonRpcMethod.getVersion()).isEqualTo(4); + assertThat(jsonRpcMethod.getVersionedName()).isEqualTo("engine_getPayloadV4"); + } + + @Test + public void executionPayloadContextParamIsRequired() { + 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 slotParamIsRequired() { + final ExecutionPayloadContext executionPayloadContext = + dataStructureUtil.randomPayloadExecutionContext(false); + + final JsonRpcRequestParams params = + new JsonRpcRequestParams.Builder().add(executionPayloadContext).build(); + + assertThatThrownBy(() -> jsonRpcMethod.execute(params)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Missing required parameter at index 1"); + + verifyNoInteractions(executionEngineClient); + } + + @Test + public void shouldReturnFailedExecutionWhenEngineClientRequestFails() { + final ExecutionPayloadContext executionPayloadContext = + dataStructureUtil.randomPayloadExecutionContext(false); + final String errorResponseFromClient = "error!"; + + when(executionEngineClient.getPayloadV4(any())) + .thenReturn(dummyFailedResponse(errorResponseFromClient)); + + final JsonRpcRequestParams params = + new JsonRpcRequestParams.Builder().add(executionPayloadContext).add(UInt64.ZERO).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 shouldCallGetPayloadV4AndParseResponseSuccessfully() { + final ExecutionPayloadContext executionPayloadContext = + dataStructureUtil.randomPayloadExecutionContext(false); + final UInt256 blockValue = UInt256.MAX_VALUE; + final BlobsBundle blobsBundle = dataStructureUtil.randomBlobsBundle(); + final ExecutionPayload executionPayloadElectra = dataStructureUtil.randomExecutionPayload(); + assertThat(executionPayloadElectra).isInstanceOf(ExecutionPayloadElectra.class); + + when(executionEngineClient.getPayloadV4(eq(executionPayloadContext.getPayloadId()))) + .thenReturn(dummySuccessfulResponse(executionPayloadElectra, blockValue, blobsBundle)); + + final JsonRpcRequestParams params = + new JsonRpcRequestParams.Builder().add(executionPayloadContext).add(UInt64.ZERO).build(); + + jsonRpcMethod = new EngineGetPayloadV4(executionEngineClient, spec); + + final GetPayloadResponse expectedGetPayloadResponse = + new GetPayloadResponse(executionPayloadElectra, blockValue, blobsBundle, false); + assertThat(jsonRpcMethod.execute(params)).isCompletedWithValue(expectedGetPayloadResponse); + + verify(executionEngineClient).getPayloadV4(eq(executionPayloadContext.getPayloadId())); + verifyNoMoreInteractions(executionEngineClient); + } + + private SafeFuture> dummySuccessfulResponse( + final ExecutionPayload executionPayload, + final UInt256 blockValue, + final BlobsBundle blobsBundle) { + return SafeFuture.completedFuture( + new Response<>( + new GetPayloadV4Response( + ExecutionPayloadV4.fromInternalExecutionPayload(executionPayload), + blockValue, + BlobsBundleV1.fromInternalBlobsBundle(blobsBundle), + false))); + } + + 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/methods/EngineNewPayloadV4Test.java b/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineNewPayloadV4Test.java new file mode 100644 index 00000000000..c27ea6f9cc6 --- /dev/null +++ b/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/methods/EngineNewPayloadV4Test.java @@ -0,0 +1,136 @@ +/* + * 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.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.apache.tuweni.bytes.Bytes32; +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.schema.ExecutionPayloadV4; +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.datastructures.execution.ExecutionPayload; +import tech.pegasys.teku.spec.executionlayer.ExecutionPayloadStatus; +import tech.pegasys.teku.spec.executionlayer.PayloadStatus; +import tech.pegasys.teku.spec.logic.versions.deneb.types.VersionedHash; +import tech.pegasys.teku.spec.util.DataStructureUtil; + +class EngineNewPayloadV4Test { + + private final Spec spec = TestSpecFactory.createMinimalElectra(); + private final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec); + private final ExecutionEngineClient executionEngineClient = mock(ExecutionEngineClient.class); + private EngineNewPayloadV4 jsonRpcMethod; + + @BeforeEach + public void setUp() { + jsonRpcMethod = new EngineNewPayloadV4(executionEngineClient); + } + + @Test + public void shouldReturnExpectedNameAndVersion() { + assertThat(jsonRpcMethod.getName()).isEqualTo("engine_newPayload"); + assertThat(jsonRpcMethod.getVersion()).isEqualTo(4); + assertThat(jsonRpcMethod.getVersionedName()).isEqualTo("engine_newPayloadV4"); + } + + @Test + public void executionPayloadParamIsRequired() { + 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 shouldReturnFailedExecutionWhenEngineClientRequestFails() { + final ExecutionPayload executionPayload = dataStructureUtil.randomExecutionPayload(); + final List blobVersionedHashes = dataStructureUtil.randomVersionedHashes(3); + final Bytes32 parentBeaconBlockRoot = dataStructureUtil.randomBytes32(); + final String errorResponseFromClient = "error!"; + + when(executionEngineClient.newPayloadV4(any(), any(), any())) + .thenReturn(dummyFailedResponse(errorResponseFromClient)); + + final JsonRpcRequestParams params = + new JsonRpcRequestParams.Builder() + .add(executionPayload) + .add(blobVersionedHashes) + .add(parentBeaconBlockRoot) + .build(); + + assertThat(jsonRpcMethod.execute(params)) + .succeedsWithin(1, TimeUnit.SECONDS) + .matches(PayloadStatus::hasFailedExecution); + } + + @Test + public void shouldCallNewPayloadV4WithExecutionPayloadV4AndBlobVersionedHashes() { + final ExecutionPayload executionPayload = dataStructureUtil.randomExecutionPayload(); + final List blobVersionedHashes = dataStructureUtil.randomVersionedHashes(4); + final Bytes32 parentBeaconBlockRoot = dataStructureUtil.randomBytes32(); + + final ExecutionPayloadV4 executionPayloadV4 = + ExecutionPayloadV4.fromInternalExecutionPayload(executionPayload); + assertThat(executionPayloadV4).isExactlyInstanceOf(ExecutionPayloadV4.class); + + jsonRpcMethod = new EngineNewPayloadV4(executionEngineClient); + + when(executionEngineClient.newPayloadV4( + executionPayloadV4, blobVersionedHashes, parentBeaconBlockRoot)) + .thenReturn(dummySuccessfulResponse()); + + final JsonRpcRequestParams params = + new JsonRpcRequestParams.Builder() + .add(executionPayload) + .add(blobVersionedHashes) + .add(parentBeaconBlockRoot) + .build(); + + assertThat(jsonRpcMethod.execute(params)).isCompleted(); + + verify(executionEngineClient) + .newPayloadV4(eq(executionPayloadV4), eq(blobVersionedHashes), eq(parentBeaconBlockRoot)); + verifyNoMoreInteractions(executionEngineClient); + } + + private SafeFuture> dummySuccessfulResponse() { + return SafeFuture.completedFuture( + new Response<>( + new PayloadStatusV1( + ExecutionPayloadStatus.ACCEPTED, dataStructureUtil.randomBytes32(), null))); + } + + private SafeFuture> dummyFailedResponse(final String errorMessage) { + return SafeFuture.completedFuture(Response.withErrorMessage(errorMessage)); + } +} 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 bb858ed6b59..22b8d6aab57 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 @@ -32,10 +32,12 @@ import tech.pegasys.teku.ethereum.executionclient.methods.EngineGetPayloadV1; import tech.pegasys.teku.ethereum.executionclient.methods.EngineGetPayloadV2; import tech.pegasys.teku.ethereum.executionclient.methods.EngineGetPayloadV3; +import tech.pegasys.teku.ethereum.executionclient.methods.EngineGetPayloadV4; import tech.pegasys.teku.ethereum.executionclient.methods.EngineJsonRpcMethod; import tech.pegasys.teku.ethereum.executionclient.methods.EngineNewPayloadV1; import tech.pegasys.teku.ethereum.executionclient.methods.EngineNewPayloadV2; import tech.pegasys.teku.ethereum.executionclient.methods.EngineNewPayloadV3; +import tech.pegasys.teku.ethereum.executionclient.methods.EngineNewPayloadV4; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.SpecMilestone; import tech.pegasys.teku.spec.datastructures.util.ForkAndSpecMilestone; @@ -111,8 +113,8 @@ private Map> denebSupportedMethods() { private Map> electraSupportedMethods() { final Map> methods = new HashMap<>(); - methods.put(ENGINE_NEW_PAYLOAD, new EngineNewPayloadV3(executionEngineClient)); - methods.put(ENGINE_GET_PAYLOAD, new EngineGetPayloadV3(executionEngineClient, spec)); + methods.put(ENGINE_NEW_PAYLOAD, new EngineNewPayloadV4(executionEngineClient)); + methods.put(ENGINE_GET_PAYLOAD, new EngineGetPayloadV4(executionEngineClient, spec)); methods.put(ENGINE_FORK_CHOICE_UPDATED, new EngineForkChoiceUpdatedV3(executionEngineClient)); return methods; 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 new file mode 100644 index 00000000000..7e940fbf04f --- /dev/null +++ b/ethereum/executionlayer/src/test/java/tech/pegasys/teku/ethereum/executionlayer/ElectraExecutionClientHandlerTest.java @@ -0,0 +1,143 @@ +/* + * 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.executionlayer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt256; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.ethereum.executionclient.schema.BlobsBundleV1; +import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV4; +import tech.pegasys.teku.ethereum.executionclient.schema.ForkChoiceStateV1; +import tech.pegasys.teku.ethereum.executionclient.schema.ForkChoiceUpdatedResult; +import tech.pegasys.teku.ethereum.executionclient.schema.GetPayloadV4Response; +import tech.pegasys.teku.ethereum.executionclient.schema.PayloadAttributesV3; +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.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.TestSpecFactory; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayload; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadContext; +import tech.pegasys.teku.spec.datastructures.execution.GetPayloadResponse; +import tech.pegasys.teku.spec.datastructures.execution.NewPayloadRequest; +import tech.pegasys.teku.spec.datastructures.execution.versions.electra.ExecutionPayloadElectra; +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.executionlayer.PayloadStatus; +import tech.pegasys.teku.spec.logic.versions.deneb.types.VersionedHash; +import tech.pegasys.teku.spec.util.DataStructureUtil; + +public class ElectraExecutionClientHandlerTest extends ExecutionHandlerClientTest { + + @BeforeEach + void setup() { + spec = TestSpecFactory.createMinimalElectra(); + dataStructureUtil = new DataStructureUtil(spec); + } + + @Test + void engineGetPayload_shouldCallGetPayloadV4() throws ExecutionException, InterruptedException { + final ExecutionClientHandler handler = getHandler(); + final ExecutionPayloadContext context = randomContext(); + final SafeFuture> dummyResponse = + SafeFuture.completedFuture( + new Response<>( + new GetPayloadV4Response( + ExecutionPayloadV4.fromInternalExecutionPayload( + dataStructureUtil.randomExecutionPayload()), + UInt256.MAX_VALUE, + BlobsBundleV1.fromInternalBlobsBundle(dataStructureUtil.randomBlobsBundle()), + true))); + when(executionEngineClient.getPayloadV4(context.getPayloadId())).thenReturn(dummyResponse); + + final UInt64 slot = dataStructureUtil.randomUInt64(1_000_000); + final SafeFuture future = handler.engineGetPayload(context, slot); + verify(executionEngineClient).getPayloadV4(context.getPayloadId()); + assertThat(future).isCompleted(); + assertThat(future.get().getExecutionPayload()).isInstanceOf(ExecutionPayloadElectra.class); + assertThat(future.get().getExecutionPayloadValue()).isEqualTo(UInt256.MAX_VALUE); + assertThat(future.get().getBlobsBundle()).isPresent(); + assertThat(future.get().getShouldOverrideBuilder()).isTrue(); + } + + @Test + void engineNewPayload_shouldCallNewPayloadV4() { + final ExecutionClientHandler handler = getHandler(); + final ExecutionPayload payload = dataStructureUtil.randomExecutionPayload(); + final Bytes32 parentBeaconBlockRoot = dataStructureUtil.randomBytes32(); + final List versionedHashes = dataStructureUtil.randomVersionedHashes(3); + final NewPayloadRequest newPayloadRequest = + new NewPayloadRequest(payload, versionedHashes, parentBeaconBlockRoot); + final ExecutionPayloadV4 payloadV4 = ExecutionPayloadV4.fromInternalExecutionPayload(payload); + final SafeFuture> dummyResponse = + SafeFuture.completedFuture( + new Response<>( + new PayloadStatusV1( + ExecutionPayloadStatus.ACCEPTED, dataStructureUtil.randomBytes32(), null))); + when(executionEngineClient.newPayloadV4(payloadV4, versionedHashes, parentBeaconBlockRoot)) + .thenReturn(dummyResponse); + final SafeFuture future = handler.engineNewPayload(newPayloadRequest); + verify(executionEngineClient).newPayloadV4(payloadV4, versionedHashes, parentBeaconBlockRoot); + assertThat(future).isCompleted(); + } + + @Test + void engineForkChoiceUpdated_shouldCallEngineForkChoiceUpdatedV3() { + final ExecutionClientHandler handler = getHandler(); + final ForkChoiceState forkChoiceState = dataStructureUtil.randomForkChoiceState(false); + final ForkChoiceStateV1 forkChoiceStateV1 = + ForkChoiceStateV1.fromInternalForkChoiceState(forkChoiceState); + final PayloadBuildingAttributes attributes = + new PayloadBuildingAttributes( + dataStructureUtil.randomUInt64(), + dataStructureUtil.randomUInt64(), + dataStructureUtil.randomUInt64(), + dataStructureUtil.randomBytes32(), + dataStructureUtil.randomEth1Address(), + Optional.empty(), + Optional.of(List.of()), + dataStructureUtil.randomBytes32()); + final Optional payloadAttributes = + PayloadAttributesV3.fromInternalPayloadBuildingAttributesV3(Optional.of(attributes)); + final SafeFuture> dummyResponse = + SafeFuture.completedFuture( + new Response<>( + new ForkChoiceUpdatedResult( + new PayloadStatusV1( + ExecutionPayloadStatus.ACCEPTED, dataStructureUtil.randomBytes32(), ""), + dataStructureUtil.randomBytes8()))); + when(executionEngineClient.forkChoiceUpdatedV3(forkChoiceStateV1, payloadAttributes)) + .thenReturn(dummyResponse); + final SafeFuture future = + handler.engineForkChoiceUpdated(forkChoiceState, Optional.of(attributes)); + verify(executionEngineClient).forkChoiceUpdatedV3(forkChoiceStateV1, payloadAttributes); + assertThat(future).isCompleted(); + } + + private ExecutionPayloadContext randomContext() { + return new ExecutionPayloadContext( + dataStructureUtil.randomBytes8(), + dataStructureUtil.randomForkChoiceState(false), + dataStructureUtil.randomPayloadBuildingAttributes(false)); + } +} diff --git a/ethereum/executionlayer/src/test/java/tech/pegasys/teku/ethereum/executionlayer/MilestoneBasedEngineJsonRpcMethodsResolverTest.java b/ethereum/executionlayer/src/test/java/tech/pegasys/teku/ethereum/executionlayer/MilestoneBasedEngineJsonRpcMethodsResolverTest.java index 750821a2bc1..be9921cc3ce 100644 --- a/ethereum/executionlayer/src/test/java/tech/pegasys/teku/ethereum/executionlayer/MilestoneBasedEngineJsonRpcMethodsResolverTest.java +++ b/ethereum/executionlayer/src/test/java/tech/pegasys/teku/ethereum/executionlayer/MilestoneBasedEngineJsonRpcMethodsResolverTest.java @@ -36,10 +36,12 @@ import tech.pegasys.teku.ethereum.executionclient.methods.EngineGetPayloadV1; import tech.pegasys.teku.ethereum.executionclient.methods.EngineGetPayloadV2; import tech.pegasys.teku.ethereum.executionclient.methods.EngineGetPayloadV3; +import tech.pegasys.teku.ethereum.executionclient.methods.EngineGetPayloadV4; import tech.pegasys.teku.ethereum.executionclient.methods.EngineJsonRpcMethod; import tech.pegasys.teku.ethereum.executionclient.methods.EngineNewPayloadV1; import tech.pegasys.teku.ethereum.executionclient.methods.EngineNewPayloadV2; import tech.pegasys.teku.ethereum.executionclient.methods.EngineNewPayloadV3; +import tech.pegasys.teku.ethereum.executionclient.methods.EngineNewPayloadV4; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.SpecMilestone; @@ -193,15 +195,16 @@ void shouldProvideExpectedMethodsForElectra( private static Stream electraMethods() { return Stream.of( - arguments(ENGINE_NEW_PAYLOAD, EngineNewPayloadV3.class), - arguments(ENGINE_GET_PAYLOAD, EngineGetPayloadV3.class), + arguments(ENGINE_NEW_PAYLOAD, EngineNewPayloadV4.class), + arguments(ENGINE_GET_PAYLOAD, EngineGetPayloadV4.class), arguments(ENGINE_FORK_CHOICE_UPDATED, EngineForkChoiceUpdatedV3.class)); } @Test void getsCapabilities() { final Spec spec = - TestSpecFactory.createMinimalWithCapellaAndDenebForkEpoch(UInt64.ONE, UInt64.valueOf(2)); + TestSpecFactory.createMinimalWithCapellaDenebAndElectraForkEpoch( + UInt64.ONE, UInt64.valueOf(2), UInt64.valueOf(3)); final MilestoneBasedEngineJsonRpcMethodsResolver engineMethodsResolver = new MilestoneBasedEngineJsonRpcMethodsResolver(spec, executionEngineClient); @@ -218,6 +221,8 @@ void getsCapabilities() { "engine_forkchoiceUpdatedV2", "engine_newPayloadV3", "engine_getPayloadV3", - "engine_forkchoiceUpdatedV3"); + "engine_forkchoiceUpdatedV3", + "engine_newPayloadV4", + "engine_getPayloadV4"); } } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/execution/ExecutionPayload.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/execution/ExecutionPayload.java index 678019f833b..848ca04357a 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/execution/ExecutionPayload.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/execution/ExecutionPayload.java @@ -25,7 +25,6 @@ import tech.pegasys.teku.spec.datastructures.execution.versions.capella.ExecutionPayloadCapella; import tech.pegasys.teku.spec.datastructures.execution.versions.capella.Withdrawal; import tech.pegasys.teku.spec.datastructures.execution.versions.deneb.ExecutionPayloadDeneb; -import tech.pegasys.teku.spec.datastructures.execution.versions.electra.ExecutionLayerExit; import tech.pegasys.teku.spec.datastructures.execution.versions.electra.ExecutionPayloadElectra; public interface ExecutionPayload extends ExecutionPayloadSummary, SszContainer, BuilderPayload { @@ -39,10 +38,6 @@ default Optional> getOptionalWithdrawals() { return Optional.empty(); } - default Optional> getOptionalExits() { - return Optional.empty(); - } - /** * getUnblindedTreeNodes * diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/executionlayer/ExecutionLayerChannelStub.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/executionlayer/ExecutionLayerChannelStub.java index 255066122a0..10418ea516e 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/executionlayer/ExecutionLayerChannelStub.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/executionlayer/ExecutionLayerChannelStub.java @@ -270,7 +270,9 @@ public SafeFuture engineGetPayload( .transactions(transactions) .withdrawals(() -> payloadAttributes.getWithdrawals().orElse(List.of())) .blobGasUsed(() -> UInt64.ZERO) - .excessBlobGas(() -> UInt64.ZERO)); + .excessBlobGas(() -> UInt64.ZERO) + .depositReceipts(List::of) + .exits(List::of)); // we assume all blocks are produced locally lastValidBlock = diff --git a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/presets/mainnet/electra.yaml b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/presets/mainnet/electra.yaml index 5ee402dd796..b920664fff2 100644 --- a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/presets/mainnet/electra.yaml +++ b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/presets/mainnet/electra.yaml @@ -1,11 +1,8 @@ # Mainnet preset - Electra -# Max operations per block -# --------------------------------------------------------------- -# 2**4 (= 16) -MAX_EXECUTION_LAYER_EXITS: 16 - # Execution # --------------------------------------------------------------- # 2**13 (= 8192) receipts -MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: 8192 \ No newline at end of file +MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: 8192 +# 2**4 (= 16) exits +MAX_EXECUTION_LAYER_EXITS: 16 \ No newline at end of file diff --git a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/presets/minimal/electra.yaml b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/presets/minimal/electra.yaml index d8a8b77dd38..ff5bd201834 100644 --- a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/presets/minimal/electra.yaml +++ b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/presets/minimal/electra.yaml @@ -1,11 +1,8 @@ # Minimal preset - Electra -# Max operations per block -# --------------------------------------------------------------- -# 2**4 (= 16) -MAX_EXECUTION_LAYER_EXITS: 16 - # Execution # --------------------------------------------------------------- # [customized] -MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: 4 \ No newline at end of file +MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: 4 +# 2**4 (= 16) exits +MAX_EXECUTION_LAYER_EXITS: 16 \ No newline at end of file diff --git a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/TestSpecFactory.java b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/TestSpecFactory.java index ee84d430d26..f7fa60b93ba 100644 --- a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/TestSpecFactory.java +++ b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/TestSpecFactory.java @@ -399,10 +399,11 @@ private static SpecConfigElectra getElectraSpecConfig( })); } - public static Spec createMinimalWithCapellaAndDenebForkEpoch( - final UInt64 capellaForkEpoch, final UInt64 denebForkEpoch) { + public static Spec createMinimalWithCapellaDenebAndElectraForkEpoch( + final UInt64 capellaForkEpoch, final UInt64 denebForkEpoch, final UInt64 electraForkEpoch) { final SpecConfigBellatrix config = - getDenebSpecConfig(Eth2Network.MINIMAL, capellaForkEpoch, denebForkEpoch); - return create(config, SpecMilestone.DENEB); + getElectraSpecConfig( + Eth2Network.MINIMAL, capellaForkEpoch, denebForkEpoch, electraForkEpoch); + return create(config, SpecMilestone.ELECTRA); } }