diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/SignedExecutionPayloadEnvelope.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/SignedExecutionPayloadEnvelope.java index 5c4de392f22..3a3780a7cb7 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/SignedExecutionPayloadEnvelope.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/SignedExecutionPayloadEnvelope.java @@ -17,6 +17,8 @@ import tech.pegasys.teku.infrastructure.logging.LogFormatter; import tech.pegasys.teku.infrastructure.ssz.containers.Container2; import tech.pegasys.teku.infrastructure.ssz.tree.TreeNode; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.blocks.SlotAndBlockRoot; import tech.pegasys.teku.spec.datastructures.type.SszSignature; public class SignedExecutionPayloadEnvelope @@ -47,6 +49,14 @@ public SignedExecutionPayloadEnvelopeSchema getSchema() { return (SignedExecutionPayloadEnvelopeSchema) super.getSchema(); } + public UInt64 getSlot() { + return getMessage().getSlot(); + } + + public SlotAndBlockRoot getSlotAndBlockRoot() { + return getMessage().getSlotAndBlockRoot(); + } + public String toLogString() { return LogFormatter.formatExecutionPayload( getMessage().getSlot(), getMessage().getBeaconBlockRoot(), getMessage().getBuilderIndex()); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/statetransition/availability/AvailabilityChecker.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/statetransition/availability/AvailabilityChecker.java index d4c3ca5b526..6841d324588 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/statetransition/availability/AvailabilityChecker.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/statetransition/availability/AvailabilityChecker.java @@ -15,10 +15,10 @@ import static tech.pegasys.teku.spec.logic.common.statetransition.availability.DataAndValidationResult.notRequiredResultFuture; +import java.util.Optional; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSidecar; -import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadHeader; import tech.pegasys.teku.spec.datastructures.execution.ExecutionProof; import tech.pegasys.teku.spec.datastructures.execution.NewPayloadRequest; import tech.pegasys.teku.spec.logic.versions.bellatrix.block.OptimisticExecutionPayloadExecutor; @@ -56,8 +56,8 @@ public SafeFuture> getAvailabilityCheckR }; /** - * Similar to {@link OptimisticExecutionPayloadExecutor#optimisticallyExecute( - * ExecutionPayloadHeader, NewPayloadRequest)} + * Similar to {@link OptimisticExecutionPayloadExecutor#optimisticallyExecute(Optional, + * NewPayloadRequest)} * * @return true if data availability check is initiated or false to immediately fail the * validation diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/statetransition/results/ExecutionPayloadImportResult.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/statetransition/results/ExecutionPayloadImportResult.java index 3c445fddb88..f4c84b7bf33 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/statetransition/results/ExecutionPayloadImportResult.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/statetransition/results/ExecutionPayloadImportResult.java @@ -18,6 +18,32 @@ public interface ExecutionPayloadImportResult { + ExecutionPayloadImportResult FAILED_UNKNOWN_BEACON_BLOCK_ROOT = + new FailedExecutionPayloadImportResult( + FailureReason.UNKNOWN_BEACON_BLOCK_ROOT, Optional.empty()); + + static ExecutionPayloadImportResult failedStateTransition(final Exception cause) { + return new FailedExecutionPayloadImportResult( + FailureReason.FAILED_STATE_TRANSITION, Optional.of(cause)); + } + + static ExecutionPayloadImportResult failedExecution(final Throwable cause) { + return new FailedExecutionPayloadImportResult( + FailureReason.FAILED_EXECUTION, Optional.of(cause)); + } + + static ExecutionPayloadImportResult failedDataAvailabilityCheckInvalid( + final Optional cause) { + return new FailedExecutionPayloadImportResult( + FailureReason.FAILED_DATA_AVAILABILITY_CHECK_INVALID, cause); + } + + static ExecutionPayloadImportResult failedDataAvailabilityCheckNotAvailable( + final Optional cause) { + return new FailedExecutionPayloadImportResult( + FailureReason.FAILED_DATA_AVAILABILITY_CHECK_NOT_AVAILABLE, cause); + } + static ExecutionPayloadImportResult internalError(final Throwable cause) { return new FailedExecutionPayloadImportResult(FailureReason.INTERNAL_ERROR, Optional.of(cause)); } @@ -28,7 +54,9 @@ static ExecutionPayloadImportResult successful( } enum FailureReason { + UNKNOWN_BEACON_BLOCK_ROOT, FAILED_STATE_TRANSITION, + FAILED_EXECUTION, FAILED_DATA_AVAILABILITY_CHECK_INVALID, FAILED_DATA_AVAILABILITY_CHECK_NOT_AVAILABLE, INTERNAL_ERROR // A catch-all category for unexpected errors (bugs) diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/util/ForkChoiceUtil.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/util/ForkChoiceUtil.java index 171178301b1..4ec574992a2 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/util/ForkChoiceUtil.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/util/ForkChoiceUtil.java @@ -31,6 +31,7 @@ import tech.pegasys.teku.spec.datastructures.blocks.BlockCheckpoints; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; import tech.pegasys.teku.spec.datastructures.blocks.blockbody.BeaconBlockBody; +import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.forkchoice.MutableStore; import tech.pegasys.teku.spec.datastructures.forkchoice.ProtoNodeData; import tech.pegasys.teku.spec.datastructures.forkchoice.ReadOnlyForkChoiceStrategy; @@ -397,6 +398,13 @@ public void applyBlockToStore( signedBlock, postState, blockCheckpoints, blobSidecars, earliestBlobSidecarsSlot); } + public void applyExecutionPayloadToStore( + final MutableStore store, + final SignedExecutionPayloadEnvelope signedEnvelope, + final BeaconState postState) { + // NO-OP until Gloas + } + private UInt64 getFinalizedCheckpointStartSlot(final ReadOnlyStore store) { final UInt64 finalizedEpoch = store.getFinalizedCheckpoint().getEpoch(); return miscHelpers.computeStartSlotAtEpoch(finalizedEpoch); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/bellatrix/block/BlockProcessorBellatrix.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/bellatrix/block/BlockProcessorBellatrix.java index d9f35ccb3f7..4b048618d91 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/bellatrix/block/BlockProcessorBellatrix.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/bellatrix/block/BlockProcessorBellatrix.java @@ -165,8 +165,7 @@ public void validateExecutionPayload( final boolean optimisticallyAccept = payloadExecutor .get() - .optimisticallyExecute( - state.getLatestExecutionPayloadHeaderRequired(), payloadToExecute); + .optimisticallyExecute(state.getLatestExecutionPayloadHeader(), payloadToExecute); if (!optimisticallyAccept) { throw new BlockProcessingException("Execution payload was not optimistically accepted"); } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/bellatrix/block/OptimisticExecutionPayloadExecutor.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/bellatrix/block/OptimisticExecutionPayloadExecutor.java index 0905fa2f5ba..97157b865fb 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/bellatrix/block/OptimisticExecutionPayloadExecutor.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/bellatrix/block/OptimisticExecutionPayloadExecutor.java @@ -13,6 +13,7 @@ package tech.pegasys.teku.spec.logic.versions.bellatrix.block; +import java.util.Optional; import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadHeader; import tech.pegasys.teku.spec.datastructures.execution.NewPayloadRequest; @@ -29,5 +30,6 @@ public interface OptimisticExecutionPayloadExecutor { * invalidate the payload */ boolean optimisticallyExecute( - ExecutionPayloadHeader latestExecutionPayloadHeader, NewPayloadRequest payloadToExecute); + Optional latestExecutionPayloadHeader, + NewPayloadRequest payloadToExecute); } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/execution/ExecutionPayloadProcessorGloas.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/execution/ExecutionPayloadProcessorGloas.java index af43276303b..94ff35d0307 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/execution/ExecutionPayloadProcessorGloas.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/execution/ExecutionPayloadProcessorGloas.java @@ -195,8 +195,7 @@ public void processUnsignedExecutionPayload( if (payloadExecutor.isPresent()) { final NewPayloadRequest payloadToExecute = computeNewPayloadRequest(state, envelope); final boolean optimisticallyAccept = - // TODO-GLOAS: https://github.com/Consensys/teku/issues/9878 - payloadExecutor.get().optimisticallyExecute(null, payloadToExecute); + payloadExecutor.get().optimisticallyExecute(Optional.empty(), payloadToExecute); if (!optimisticallyAccept) { throw new ExecutionPayloadProcessingException( "Execution payload was not optimistically accepted"); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/util/ForkChoiceUtilGloas.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/util/ForkChoiceUtilGloas.java index 68fc31cf0a0..b72718e1b1a 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/util/ForkChoiceUtilGloas.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/util/ForkChoiceUtilGloas.java @@ -13,26 +13,36 @@ package tech.pegasys.teku.spec.logic.versions.gloas.util; -import tech.pegasys.teku.spec.config.SpecConfig; +import tech.pegasys.teku.spec.config.SpecConfigGloas; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; -import tech.pegasys.teku.spec.logic.common.helpers.BeaconStateAccessors; -import tech.pegasys.teku.spec.logic.common.helpers.MiscHelpers; +import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; +import tech.pegasys.teku.spec.datastructures.forkchoice.MutableStore; +import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; import tech.pegasys.teku.spec.logic.common.statetransition.availability.AvailabilityChecker; -import tech.pegasys.teku.spec.logic.common.statetransition.epoch.EpochProcessor; -import tech.pegasys.teku.spec.logic.common.util.AttestationUtil; import tech.pegasys.teku.spec.logic.versions.fulu.util.ForkChoiceUtilFulu; +import tech.pegasys.teku.spec.logic.versions.gloas.helpers.BeaconStateAccessorsGloas; +import tech.pegasys.teku.spec.logic.versions.gloas.helpers.MiscHelpersGloas; +import tech.pegasys.teku.spec.logic.versions.gloas.statetransition.epoch.EpochProcessorGloas; public class ForkChoiceUtilGloas extends ForkChoiceUtilFulu { public ForkChoiceUtilGloas( - final SpecConfig specConfig, - final BeaconStateAccessors beaconStateAccessors, - final EpochProcessor epochProcessor, - final AttestationUtil attestationUtil, - final MiscHelpers miscHelpers) { + final SpecConfigGloas specConfig, + final BeaconStateAccessorsGloas beaconStateAccessors, + final EpochProcessorGloas epochProcessor, + final AttestationUtilGloas attestationUtil, + final MiscHelpersGloas miscHelpers) { super(specConfig, beaconStateAccessors, epochProcessor, attestationUtil, miscHelpers); } + @Override + public void applyExecutionPayloadToStore( + final MutableStore store, + final SignedExecutionPayloadEnvelope signedEnvelope, + final BeaconState postState) { + // TODO-GLOAS: https://github.com/Consensys/teku/issues/9878 + } + @Override public AvailabilityChecker createAvailabilityChecker(final SignedBeaconBlock block) { // TODO-GLOAS: in ePBS, data availability is delayed until the processing of the execution diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java index 3c167109494..b7e27cfb95a 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java @@ -225,13 +225,13 @@ public SafeFuture onBlock( executionLayer)); } - // TODO-GLOAS: https://github.com/Consensys/teku/issues/9878 /** Import an execution payload to the store. */ public SafeFuture onExecutionPayload( final SignedExecutionPayloadEnvelope signedEnvelope, final ExecutionLayerChannel executionLayer) { - // just for local interop temporarily return successful import - return SafeFuture.completedFuture(ExecutionPayloadImportResult.successful(signedEnvelope)); + return recentChainData + .retrieveStateAtSlot(signedEnvelope.getSlotAndBlockRoot()) + .thenCompose(blockState -> onExecutionPayload(signedEnvelope, blockState, executionLayer)); } public SafeFuture onAttestation( @@ -519,6 +519,70 @@ private SafeFuture onBlock( }); } + /** + * Import an execution payload to the store. The supplied {@code blockState} must be the + * post-state after processing the block whose root is the beacon block root of the execution + * payload + */ + private SafeFuture onExecutionPayload( + final SignedExecutionPayloadEnvelope signedEnvelope, + final Optional blockState, + final ExecutionLayerChannel executionLayer) { + if (blockState.isEmpty()) { + return SafeFuture.completedFuture( + ExecutionPayloadImportResult.FAILED_UNKNOWN_BEACON_BLOCK_ROOT); + } + + final ForkChoiceUtil forkChoiceUtil = spec.atSlot(signedEnvelope.getSlot()).getForkChoiceUtil(); + + // TODO-GLOAS: https://github.com/Consensys/teku/issues/9878 add a real data availability check + // (not required for devnet-0) + final AvailabilityChecker availabilityChecker = AvailabilityChecker.NOOP_DATACOLUMN_SIDECAR; + + availabilityChecker.initiateDataAvailabilityCheck(); + + final ForkChoicePayloadExecutorGloas payloadExecutor = + ForkChoicePayloadExecutorGloas.create(signedEnvelope, executionLayer); + + final BeaconState postState; + try { + postState = + spec.getExecutionPayloadProcessor(signedEnvelope.getSlot()) + .processAndVerifyExecutionPayload( + signedEnvelope, blockState.get(), Optional.of(payloadExecutor)); + } catch (final StateTransitionException ex) { + final ExecutionPayloadImportResult result = + ExecutionPayloadImportResult.failedStateTransition(ex); + reportInvalidExecutionPayload(signedEnvelope, result); + return SafeFuture.completedFuture(result); + } + + final SafeFuture> dataAndValidationResultFuture = + availabilityChecker + .getAvailabilityCheckResult() + .thenPeek( + result -> + LOG.debug( + "Data availability check for slot: {}, builder: {}, block_root: {} result: {}", + signedEnvelope.getSlot(), + signedEnvelope.getMessage().getBuilderIndex(), + signedEnvelope.getMessage().getBeaconBlockRoot(), + result.toLogString())); + + return payloadExecutor + .getExecutionResult() + .thenCombineAsync( + dataAndValidationResultFuture, + (payloadResult, dataAndValidationResult) -> + importExecutionPayloadAndState( + signedEnvelope, + forkChoiceUtil, + postState, + payloadResult, + dataAndValidationResult), + forkChoiceExecutor); + } + private BlockImportResult importBlockAndState( final SignedBeaconBlock block, final BeaconState blockSlotState, @@ -562,15 +626,11 @@ private BlockImportResult importBlockAndState( } switch (dataAndValidationResult.validationResult()) { - case VALID, NOT_REQUIRED -> - LOG.debug("Sidecars validation result: {}", dataAndValidationResult::toLogString); case NOT_AVAILABLE -> { - LOG.debug("Sidecars validation result: {}", dataAndValidationResult::toLogString); return BlockImportResult.failedDataAvailabilityCheckNotAvailable( dataAndValidationResult.cause()); } case INVALID -> { - LOG.debug("Sidecars validation result: {}", dataAndValidationResult::toLogString); debugDataDumper.saveInvalidSidecars(dataAndValidationResult.data(), block); return BlockImportResult.failedDataAvailabilityCheckInvalid( dataAndValidationResult.cause()); @@ -640,6 +700,53 @@ private BlockImportResult importBlockAndState( return result; } + // TODO-GLOAS: https://github.com/Consensys/teku/issues/9878 it requires potentially more + // validations and more interactions with the store + private ExecutionPayloadImportResult importExecutionPayloadAndState( + final SignedExecutionPayloadEnvelope signedEnvelope, + final ForkChoiceUtil forkChoiceUtil, + final BeaconState postState, + final PayloadValidationResult payloadResult, + final DataAndValidationResult dataAndValidationResult) { + final PayloadStatus payloadStatus = payloadResult.getStatus(); + + if (payloadStatus.hasInvalidStatus()) { + final ExecutionPayloadImportResult result = + ExecutionPayloadImportResult.failedStateTransition( + new IllegalStateException( + "Invalid ExecutionPayload: " + + payloadStatus.getValidationError().orElse("No reason provided"))); + reportInvalidExecutionPayload(signedEnvelope, result); + return result; + } + + if (payloadStatus.hasFailedExecution()) { + return ExecutionPayloadImportResult.failedExecution( + payloadStatus.getFailureCause().orElseThrow()); + } + + switch (dataAndValidationResult.validationResult()) { + case NOT_AVAILABLE -> { + return ExecutionPayloadImportResult.failedDataAvailabilityCheckNotAvailable( + dataAndValidationResult.cause()); + } + case INVALID -> { + return ExecutionPayloadImportResult.failedDataAvailabilityCheckInvalid( + dataAndValidationResult.cause()); + } + default -> {} + } + + final StoreTransaction transaction = recentChainData.startStoreTransaction(); + + forkChoiceUtil.applyExecutionPayloadToStore(transaction, signedEnvelope, postState); + + // Note: not using thenRun here because we want to ensure each step is on the event thread + transaction.commit().join(); + + return ExecutionPayloadImportResult.successful(signedEnvelope); + } + // from consensus-specs/fork-choice: private boolean shouldApplyProposerBoost( final SignedBeaconBlock block, final StoreTransaction transaction) { @@ -806,6 +913,20 @@ private void reportInvalidBlock(final SignedBeaconBlock block, final BlockImport result.getFailureCause()); } + private void reportInvalidExecutionPayload( + final SignedExecutionPayloadEnvelope signedEnvelope, + final ExecutionPayloadImportResult result) { + debugDataDumper.saveInvalidExecutionPayload( + signedEnvelope, result.getFailureReason().name(), result.getFailureCause()); + P2P_LOG.onInvalidExecutionPayload( + signedEnvelope.getSlot(), + signedEnvelope.getMessage().getBuilderIndex(), + signedEnvelope.getMessage().getBeaconBlockRoot(), + signedEnvelope.sszSerialize(), + result.getFailureReason().name(), + result.getFailureCause()); + } + private void notifyForkChoiceUpdatedAndOptimisticSyncingChanged( final Optional proposingSlot) { final ForkChoiceState forkChoiceState = forkChoiceStateProvider.getForkChoiceStateSync(); diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoicePayloadExecutor.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoicePayloadExecutor.java index 1788b1a5c19..3fee0b1573e 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoicePayloadExecutor.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoicePayloadExecutor.java @@ -33,6 +33,7 @@ class ForkChoicePayloadExecutor implements OptimisticExecutionPayloadExecutor { private final ExecutionLayerChannel executionLayer; private final SignedBeaconBlock block; private final MergeTransitionBlockValidator transitionBlockValidator; + private Optional> result = Optional.empty(); ForkChoicePayloadExecutor( @@ -60,7 +61,7 @@ public SafeFuture getExecutionResult() { @Override public boolean optimisticallyExecute( - final ExecutionPayloadHeader latestExecutionPayloadHeader, + final Optional latestExecutionPayloadHeader, final NewPayloadRequest payloadToExecute) { final ExecutionPayload executionPayload = payloadToExecute.getExecutionPayload(); if (executionPayload.isDefault()) { @@ -77,7 +78,7 @@ public boolean optimisticallyExecute( result -> { if (result.hasValidStatus()) { return transitionBlockValidator.verifyTransitionBlock( - latestExecutionPayloadHeader, block); + latestExecutionPayloadHeader.orElseThrow(), block); } else { return SafeFuture.completedFuture(new PayloadValidationResult(result)); } diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoicePayloadExecutorGloas.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoicePayloadExecutorGloas.java new file mode 100644 index 00000000000..6aa284e1ed4 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoicePayloadExecutorGloas.java @@ -0,0 +1,69 @@ +/* + * Copyright Consensys Software Inc., 2025 + * + * 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.statetransition.forkchoice; + +import java.util.Optional; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadHeader; +import tech.pegasys.teku.spec.datastructures.execution.NewPayloadRequest; +import tech.pegasys.teku.spec.executionlayer.ExecutionLayerChannel; +import tech.pegasys.teku.spec.executionlayer.PayloadStatus; +import tech.pegasys.teku.spec.logic.versions.bellatrix.block.OptimisticExecutionPayloadExecutor; + +class ForkChoicePayloadExecutorGloas implements OptimisticExecutionPayloadExecutor { + private static final Logger LOG = LogManager.getLogger(); + + private final SignedExecutionPayloadEnvelope signedEnvelope; + private final ExecutionLayerChannel executionLayer; + + private Optional> result = Optional.empty(); + + ForkChoicePayloadExecutorGloas( + final SignedExecutionPayloadEnvelope signedEnvelope, + final ExecutionLayerChannel executionLayer) { + this.signedEnvelope = signedEnvelope; + this.executionLayer = executionLayer; + } + + public static ForkChoicePayloadExecutorGloas create( + final SignedExecutionPayloadEnvelope signedEnvelope, + final ExecutionLayerChannel executionLayer) { + return new ForkChoicePayloadExecutorGloas(signedEnvelope, executionLayer); + } + + public SafeFuture getExecutionResult() { + return result.orElse( + SafeFuture.completedFuture(new PayloadValidationResult(PayloadStatus.VALID))); + } + + @Override + public boolean optimisticallyExecute( + final Optional latestExecutionPayloadHeader, + final NewPayloadRequest payloadToExecute) { + result = + Optional.of( + executionLayer + .engineNewPayload(payloadToExecute, signedEnvelope.getSlot()) + .thenApply(PayloadValidationResult::new) + .exceptionally( + error -> { + LOG.error("Error while validating payload", error); + return new PayloadValidationResult(PayloadStatus.failedExecution(error)); + })); + return true; + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/util/DebugDataDumper.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/util/DebugDataDumper.java index 71282d1f3ae..cc076a0d406 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/util/DebugDataDumper.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/util/DebugDataDumper.java @@ -19,6 +19,7 @@ import org.apache.tuweni.bytes.Bytes; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; +import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; public interface DebugDataDumper { @@ -46,6 +47,12 @@ public void saveInvalidBlock( @Override public void saveInvalidSidecars(final List sidecars, final SignedBeaconBlock block) {} + + @Override + public void saveInvalidExecutionPayload( + final SignedExecutionPayloadEnvelope signedEnvelope, + final String failureReason, + final Optional failureCause) {} }; void saveGossipMessageDecodingError( @@ -64,4 +71,9 @@ void saveInvalidBlock( SignedBeaconBlock block, String failureReason, Optional failureCause); void saveInvalidSidecars(List sidecars, SignedBeaconBlock block); + + void saveInvalidExecutionPayload( + SignedExecutionPayloadEnvelope signedEnvelope, + String failureReason, + Optional failureCause); } diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/util/DebugDataFileDumper.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/util/DebugDataFileDumper.java index 2fd1999ce4c..a8a7b773485 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/util/DebugDataFileDumper.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/util/DebugDataFileDumper.java @@ -30,11 +30,15 @@ import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; +import tech.pegasys.teku.infrastructure.ssz.SszList; import tech.pegasys.teku.infrastructure.time.TimeProvider; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.datastructures.blobs.DataColumnSidecar; import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSidecar; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; +import tech.pegasys.teku.spec.datastructures.blocks.SlotAndBlockRoot; +import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; +import tech.pegasys.teku.spec.datastructures.type.SszKZGCommitment; public class DebugDataFileDumper implements DebugDataDumper { @@ -46,6 +50,7 @@ public class DebugDataFileDumper implements DebugDataDumper { private static final String INVALID_BLOCK_DIR = "invalid_blocks"; private static final String INVALID_BLOB_SIDECARS_DIR = "invalid_blob_sidecars"; private static final String INVALID_DATA_COLUMN_SIDECARS_DIR = "invalid_data_column_sidecars"; + private static final String INVALID_EXECUTION_PAYLOAD_DIR = "invalid_execution_payloads"; private boolean enabled; private final Path directory; @@ -152,18 +157,49 @@ public void saveInvalidSidecars(final List sidecars, final SignedBeaconBlock if (!enabled || sidecars.isEmpty()) { return; } - if (sidecars.getFirst() instanceof BlobSidecar) { saveInvalidBlobSidecars((List) sidecars, block); } else if (sidecars.getFirst() instanceof DataColumnSidecar) { - saveInvalidDataColumnSidecars((List) sidecars, block); + saveInvalidDataColumnSidecars( + (List) sidecars, + block.getSlotAndBlockRoot(), + block.getMessage().getBody().getOptionalBlobKzgCommitments().orElseThrow()); } else { throw new RuntimeException("Unknown sidecar type: " + sidecars.getFirst()); } } + @Override + public void saveInvalidExecutionPayload( + final SignedExecutionPayloadEnvelope signedEnvelope, + final String failureReason, + final Optional failureCause) { + if (!enabled) { + return; + } + final UInt64 slot = signedEnvelope.getSlot(); + final Bytes32 blockRoot = signedEnvelope.getMessage().getBeaconBlockRoot(); + final UInt64 builderIndex = signedEnvelope.getMessage().getBuilderIndex(); + final String fileName = + String.format("%s_%s_%s.ssz", slot, blockRoot.toUnprefixedHexString(), builderIndex); + final boolean success = + saveBytesToFile( + "invalid execution payload", + Path.of(INVALID_EXECUTION_PAYLOAD_DIR).resolve(fileName), + signedEnvelope.sszSerialize()); + if (success) { + LOG.warn( + "Rejecting invalid execution payload at slot {} with block root {} and builder index {}, reason: {}, cause: {}", + slot, + blockRoot, + builderIndex, + failureReason, + failureCause.orElse(null)); + } + } + private void saveInvalidBlobSidecars( - final List sidecars, final SignedBeaconBlock block) { + final List blobSidecars, final SignedBeaconBlock block) { final String kzgCommitmentsFileName = String.format( "%s_%s_kzg_commitments.ssz", block.getSlot(), block.getRoot().toUnprefixedHexString()); @@ -172,41 +208,44 @@ private void saveInvalidBlobSidecars( Path.of(INVALID_BLOB_SIDECARS_DIR).resolve(kzgCommitmentsFileName), block.getMessage().getBody().getOptionalBlobKzgCommitments().orElseThrow().sszSerialize()); - sidecars.forEach( - sidecar -> { - final UInt64 slot = sidecar.getSlot(); - final Bytes32 blockRoot = sidecar.getBlockRoot(); - final UInt64 index = sidecar.getIndex(); + blobSidecars.forEach( + blobSidecar -> { + final UInt64 slot = blobSidecar.getSlot(); + final Bytes32 blockRoot = blobSidecar.getBlockRoot(); + final UInt64 index = blobSidecar.getIndex(); final String fileName = String.format("%s_%s_%s.ssz", slot, blockRoot.toUnprefixedHexString(), index); saveBytesToFile( "blob sidecar", Path.of(INVALID_BLOB_SIDECARS_DIR).resolve(fileName), - sidecar.sszSerialize()); + blobSidecar.sszSerialize()); }); } private void saveInvalidDataColumnSidecars( - final List sidecars, final SignedBeaconBlock block) { + final List dataColumnSidecars, + final SlotAndBlockRoot slotAndBlockRoot, + final SszList blobKzgCommitments) { final String kzgCommitmentsFileName = String.format( - "%s_%s_kzg_commitments.ssz", block.getSlot(), block.getRoot().toUnprefixedHexString()); + "%s_%s_kzg_commitments.ssz", + slotAndBlockRoot.getSlot(), slotAndBlockRoot.getBlockRoot().toUnprefixedHexString()); saveBytesToFile( "kzg commitments", Path.of(INVALID_DATA_COLUMN_SIDECARS_DIR).resolve(kzgCommitmentsFileName), - block.getMessage().getBody().getOptionalBlobKzgCommitments().orElseThrow().sszSerialize()); + blobKzgCommitments.sszSerialize()); - sidecars.forEach( - sidecar -> { - final UInt64 slot = sidecar.getSlot(); - final Bytes32 blockRoot = sidecar.getBeaconBlockRoot(); - final UInt64 index = sidecar.getIndex(); + dataColumnSidecars.forEach( + dataColumnSidecar -> { + final UInt64 slot = dataColumnSidecar.getSlot(); + final Bytes32 blockRoot = dataColumnSidecar.getBeaconBlockRoot(); + final UInt64 index = dataColumnSidecar.getIndex(); final String fileName = String.format("%s_%s_%s.ssz", slot, blockRoot.toUnprefixedHexString(), index); saveBytesToFile( "data column sidecar", Path.of(INVALID_DATA_COLUMN_SIDECARS_DIR).resolve(fileName), - sidecar.sszSerialize()); + dataColumnSidecar.sszSerialize()); }); } diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoicePayloadExecutorGloasTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoicePayloadExecutorGloasTest.java new file mode 100644 index 00000000000..138b957b466 --- /dev/null +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoicePayloadExecutorGloasTest.java @@ -0,0 +1,90 @@ +/* + * Copyright Consensys Software Inc., 2025 + * + * 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.statetransition.forkchoice; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static tech.pegasys.teku.spec.executionlayer.PayloadStatus.VALID; + +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.bls.BLSSignatureVerifier; +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.epbs.versions.gloas.SignedExecutionPayloadEnvelope; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayload; +import tech.pegasys.teku.spec.datastructures.execution.NewPayloadRequest; +import tech.pegasys.teku.spec.executionlayer.ExecutionLayerChannel; +import tech.pegasys.teku.spec.executionlayer.PayloadStatus; +import tech.pegasys.teku.spec.util.DataStructureUtil; + +class ForkChoicePayloadExecutorGloasTest { + + private final Spec spec = + TestSpecFactory.createMinimalGloas( + builder -> builder.blsSignatureVerifier(BLSSignatureVerifier.NO_OP)); + private final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec); + private final SafeFuture executionResult = new SafeFuture<>(); + private final ExecutionLayerChannel executionLayer = mock(ExecutionLayerChannel.class); + private final ExecutionPayload payload = dataStructureUtil.randomExecutionPayload(); + private final SignedExecutionPayloadEnvelope signedEnvelope = + dataStructureUtil.randomSignedExecutionPayloadEnvelope(0); + private final NewPayloadRequest payloadRequest = new NewPayloadRequest(payload); + + private final ForkChoicePayloadExecutorGloas payloadExecutor = + new ForkChoicePayloadExecutorGloas(signedEnvelope, executionLayer); + + @BeforeEach + void setUp() { + when(executionLayer.engineNewPayload(any(), any())).thenReturn(executionResult); + } + + @Test + void optimisticallyExecute_shouldSendToExecutionEngineAndReturnTrue() { + final boolean result = payloadExecutor.optimisticallyExecute(Optional.empty(), payloadRequest); + verify(executionLayer).engineNewPayload(payloadRequest, UInt64.ZERO); + assertThat(result).isTrue(); + } + + @Test + void optimisticallyExecute_shouldReturnFailedExecutionWhenELOfflineAtExecution() { + when(executionLayer.engineNewPayload(payloadRequest, UInt64.ZERO)) + .thenReturn(SafeFuture.failedFuture(new Error())); + final boolean execution = + payloadExecutor.optimisticallyExecute(Optional.empty(), payloadRequest); + + verify(executionLayer).engineNewPayload(payloadRequest, UInt64.ZERO); + assertThat(execution).isTrue(); + assertThat(payloadExecutor.getExecutionResult()) + .isCompletedWithValueMatching(result -> result.getStatus().hasFailedExecution()); + } + + @Test + void shouldReturnExecutionResultWhenExecuted() { + payloadExecutor.optimisticallyExecute(Optional.empty(), payloadRequest); + + final SafeFuture result = payloadExecutor.getExecutionResult(); + assertThat(result).isNotCompleted(); + + this.executionResult.complete(VALID); + + assertThat(result).isCompletedWithValue(PayloadValidationResult.VALID); + } +} diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoicePayloadExecutorTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoicePayloadExecutorTest.java index b96671e19b8..95500616a0a 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoicePayloadExecutorTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoicePayloadExecutorTest.java @@ -69,7 +69,8 @@ void setUp() { @Test void optimisticallyExecute_shouldSendToExecutionEngineAndReturnTrue() { final ForkChoicePayloadExecutor payloadExecutor = createPayloadExecutor(); - final boolean result = payloadExecutor.optimisticallyExecute(payloadHeader, payloadRequest); + final boolean result = + payloadExecutor.optimisticallyExecute(Optional.of(payloadHeader), payloadRequest); verify(executionLayer).engineNewPayload(payloadRequest, UInt64.ZERO); assertThat(result).isTrue(); } @@ -78,7 +79,7 @@ void optimisticallyExecute_shouldSendToExecutionEngineAndReturnTrue() { void optimisticallyExecute_shouldNotExecuteDefaultPayload() { final ForkChoicePayloadExecutor payloadExecutor = createPayloadExecutor(); final boolean result = - payloadExecutor.optimisticallyExecute(payloadHeader, defaultPayloadRequest); + payloadExecutor.optimisticallyExecute(Optional.of(payloadHeader), defaultPayloadRequest); verify(executionLayer, never()).engineNewPayload(any(), any()); assertThat(result).isTrue(); assertThat(payloadExecutor.getExecutionResult()) @@ -92,7 +93,7 @@ void optimisticallyExecute_shouldValidateMergeBlockWhenThisIsTheMergeBlock() { when(executionLayer.eth1GetPowBlock(payload.getParentHash())).thenReturn(new SafeFuture<>()); final ForkChoicePayloadExecutor payloadExecutor = createPayloadExecutor(); final boolean result = - payloadExecutor.optimisticallyExecute(defaultPayloadHeader, payloadRequest); + payloadExecutor.optimisticallyExecute(Optional.of(defaultPayloadHeader), payloadRequest); // Should execute first and then begin validation of the transition block conditions. verify(executionLayer).engineNewPayload(payloadRequest, UInt64.ZERO); @@ -106,7 +107,7 @@ void optimisticallyExecute_shouldReturnFailedExecutionOnMergeBlockWhenELOfflineA .thenReturn(SafeFuture.failedFuture(new Error())); final ForkChoicePayloadExecutor payloadExecutor = createPayloadExecutor(); final boolean execution = - payloadExecutor.optimisticallyExecute(defaultPayloadHeader, payloadRequest); + payloadExecutor.optimisticallyExecute(Optional.of(defaultPayloadHeader), payloadRequest); // Should not attempt to validate transition conditions because execute payload failed verify(transitionValidator, never()).verifyTransitionBlock(defaultPayloadHeader, block); @@ -125,7 +126,7 @@ void optimisticallyExecute_shouldReturnFailedExecutionOnMergeBlockWhenELOfflineA .thenReturn(SafeFuture.failedFuture(new Error())); final ForkChoicePayloadExecutor payloadExecutor = createPayloadExecutor(); final boolean execution = - payloadExecutor.optimisticallyExecute(defaultPayloadHeader, payloadRequest); + payloadExecutor.optimisticallyExecute(Optional.of(defaultPayloadHeader), payloadRequest); verify(transitionValidator).verifyTransitionBlock(defaultPayloadHeader, block); verify(executionLayer).engineNewPayload(payloadRequest, UInt64.ZERO); @@ -142,7 +143,7 @@ void optimisticallyExecute_shouldNotVerifyTransitionIfExecutePayloadIsInvalid() .thenReturn(SafeFuture.completedFuture(expectedResult)); final ForkChoicePayloadExecutor payloadExecutor = createPayloadExecutor(); final boolean execution = - payloadExecutor.optimisticallyExecute(defaultPayloadHeader, payloadRequest); + payloadExecutor.optimisticallyExecute(Optional.of(defaultPayloadHeader), payloadRequest); verify(executionLayer).engineNewPayload(payloadRequest, UInt64.ZERO); verify(transitionValidator, never()).verifyTransitionBlock(defaultPayloadHeader, block); @@ -164,7 +165,7 @@ void shouldReturnExecutionResultWhenExecuted() { when(transitionValidator.verifyTransitionBlock(payloadHeader, block)) .thenReturn(SafeFuture.completedFuture(PayloadValidationResult.VALID)); final ForkChoicePayloadExecutor payloadExecutor = createPayloadExecutor(); - payloadExecutor.optimisticallyExecute(payloadHeader, payloadRequest); + payloadExecutor.optimisticallyExecute(Optional.of(payloadHeader), payloadRequest); final SafeFuture result = payloadExecutor.getExecutionResult(); assertThat(result).isNotCompleted(); diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/util/DebugDataFileDumperTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/util/DebugDataFileDumperTest.java index 227dabe3ad7..95561951c62 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/util/DebugDataFileDumperTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/util/DebugDataFileDumperTest.java @@ -36,10 +36,11 @@ import tech.pegasys.teku.spec.TestSpecFactory; import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSidecar; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; +import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.util.DataStructureUtil; class DebugDataFileDumperTest { - final DataStructureUtil dataStructureUtil = + private DataStructureUtil dataStructureUtil = new DataStructureUtil(TestSpecFactory.createMinimalDeneb()); private final StubTimeProvider timeProvider = StubTimeProvider.withTimeInSeconds(10_000); @@ -174,6 +175,24 @@ void formatOptionalTimestamp_shouldGenerateTimestamp(@TempDir final Path tempDir .isEqualTo(formatTimestamp(timeProvider.getTimeInMillis().longValue())); } + @Test + void saveInvalidExecutionPayloadToFile_shouldSaveToFile(@TempDir final Path tempDir) { + dataStructureUtil = new DataStructureUtil(TestSpecFactory.createMinimalGloas()); + final DebugDataFileDumper dumper = new DebugDataFileDumper(tempDir); + final SignedExecutionPayloadEnvelope executionPayload = + dataStructureUtil.randomSignedExecutionPayloadEnvelope(42); + dumper.saveInvalidExecutionPayload(executionPayload, "reason", Optional.of(new Throwable())); + + final String fileName = + String.format( + "%s_%s_%s.ssz", + executionPayload.getSlot(), + executionPayload.getMessage().getBeaconBlockRoot().toUnprefixedHexString(), + executionPayload.getMessage().getBuilderIndex()); + final Path expectedFile = tempDir.resolve("invalid_execution_payloads").resolve(fileName); + checkBytesSavedToFile(expectedFile, executionPayload.sszSerialize()); + } + private void checkBytesSavedToFile(final Path path, final Bytes expectedBytes) { try { final Bytes bytes = Bytes.wrap(Files.readAllBytes(path)); diff --git a/infrastructure/logging/src/main/java/tech/pegasys/teku/infrastructure/logging/P2PLogger.java b/infrastructure/logging/src/main/java/tech/pegasys/teku/infrastructure/logging/P2PLogger.java index fc1b54d9e56..4fddfa8eaec 100644 --- a/infrastructure/logging/src/main/java/tech/pegasys/teku/infrastructure/logging/P2PLogger.java +++ b/infrastructure/logging/src/main/java/tech/pegasys/teku/infrastructure/logging/P2PLogger.java @@ -70,4 +70,23 @@ public void onInvalidBlock( failureCause.orElse(null)); } } + + public void onInvalidExecutionPayload( + final UInt64 slot, + final UInt64 builderIndex, + final Bytes32 blockRoot, + final Bytes executionPayloadSsz, + final String failureReason, + final Optional failureCause) { + if (isIncludeP2pWarnings) { + log.warn( + "Rejecting invalid execution payload at slot {} with builder {} and block root {} because {}. Full execution payload data: {}", + slot, + builderIndex, + blockRoot, + failureReason, + executionPayloadSsz, + failureCause.orElse(null)); + } + } }