From ab98ca759954f042c1ec0579bbdca9de8254a0b4 Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Mon, 26 Jan 2026 15:21:19 +0100 Subject: [PATCH] eth/catalyst: implement changes for EPBs devnets Add Engine API method stubs for the Amsterdam fork (Gloas on CL side): - engine_newPayloadV5 for Amsterdam payloads - engine_getPayloadV6 for Amsterdam payloads - engine_newPayloadWithWitnessV5 for witness generation - engine_executeStatelessPayloadV5 for stateless execution - Update engine_forkchoiceUpdatedV3 to accept Amsterdam payloads - Add PayloadV4 constant for ExecutionPayloadV4 This is a minimal implementation with method stubs only - no blockAccessList (EIP-7928) integration yet. Co-Authored-By: Claude Opus 4.5 --- beacon/engine/types.go | 2 +- eth/catalyst/api.go | 24 +++++++++++++----- eth/catalyst/witness.go | 56 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/beacon/engine/types.go b/beacon/engine/types.go index a312fee88a..c94dde4248 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -55,7 +55,7 @@ var ( // // https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#executionpayloadv4 // ExecutionPayloadV4 has the syntax of ExecutionPayloadV3 and appends the new - // field slotNumber. + // fields slotNumber and blockAccessList (EIP-7928). PayloadV4 PayloadVersion = 0x4 ) diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 8a4aced04b..a1bb68e012 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -201,8 +201,8 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV3(ctx context.Context, update engine. return engine.STATUS_INVALID, attributesErr("missing withdrawals") case params.BeaconRoot == nil: return engine.STATUS_INVALID, attributesErr("missing beacon root") - case !api.checkFork(params.Timestamp, forks.Cancun, forks.Prague, forks.Osaka, forks.BPO1, forks.BPO2, forks.BPO3, forks.BPO4, forks.BPO5): - return engine.STATUS_INVALID, unsupportedForkErr("fcuV3 must only be called for cancun/prague/osaka payloads") + case !api.checkFork(params.Timestamp, forks.Cancun, forks.Prague, forks.Osaka, forks.BPO1, forks.Amsterdam, forks.BPO2, forks.BPO3, forks.BPO4, forks.BPO5): + return engine.STATUS_INVALID, unsupportedForkErr("fcuV3 must only be called for cancun/prague/osaka/amsterdam payloads") } } // TODO(matt): the spec requires that fcu is applied when called on a valid @@ -323,6 +323,12 @@ func (api *ConsensusAPI) forkchoiceUpdated(ctx context.Context, update engine.Fo } else { // If the head block is already in our canonical chain, the beacon client is // probably resyncing. Ignore the update. + // TODO(MariusVanDerWijden): in epbs it can happen that we are reorging to a previous head + if api.eth.Synced() { + if latestValid, err := api.eth.BlockChain().SetCanonical(block); err != nil { + return engine.ForkChoiceResponse{PayloadStatus: engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &latestValid}}, err + } + } log.Info("Ignoring beacon update to old head", "number", block.NumberU64(), "hash", update.HeadBlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)), "have", api.eth.BlockChain().CurrentBlock().Number) return valid(nil), nil } @@ -479,19 +485,23 @@ func (api *ConsensusAPI) GetPayloadV5(payloadID engine.PayloadID) (*engine.Execu forks.BPO3, forks.BPO4, forks.BPO5, + forks.Amsterdam, // TODO (MariusVanDerWijden) remove + // Amsterdam uses GetPayloadV6 }) } // GetPayloadV6 returns a cached payload by id. This endpoint should only // be used after the Amsterdam fork. +// +// This method follows the same specification as engine_getPayloadV5 with +// changes of returning ExecutionPayloadV4 with blockAccessList. func (api *ConsensusAPI) GetPayloadV6(payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) { return api.getPayload( payloadID, false, []engine.PayloadVersion{engine.PayloadV4}, - []forks.Fork{ - forks.Amsterdam, - }) + []forks.Fork{forks.Amsterdam}, + ) } // getPayload will retrieve the specified payload and verify it conforms to the @@ -726,7 +736,9 @@ func (api *ConsensusAPI) NewPayloadV4(ctx context.Context, params engine.Executa return invalidStatus, paramsErr("nil beaconRoot post-cancun") case executionRequests == nil: return invalidStatus, paramsErr("nil executionRequests post-prague") - case !api.checkFork(params.Timestamp, forks.Prague, forks.Osaka, forks.BPO1, forks.BPO2, forks.BPO3, forks.BPO4, forks.BPO5): + case api.checkFork(params.Timestamp, forks.Amsterdam): + return invalidStatus, unsupportedForkErr("newPayloadV4 must not be called for amsterdam payloads, use newPayloadV5") + case !api.checkFork(params.Timestamp, forks.Prague, forks.Osaka, forks.BPO1, forks.BPO2, forks.BPO3, forks.BPO4, forks.BPO5, forks.Amsterdam): // TODO: remove Amsterdam once it is activated return invalidStatus, unsupportedForkErr("newPayloadV4 must only be called for prague/osaka payloads") } requests := convertRequests(executionRequests) diff --git a/eth/catalyst/witness.go b/eth/catalyst/witness.go index fe75c66908..f406e277f1 100644 --- a/eth/catalyst/witness.go +++ b/eth/catalyst/witness.go @@ -74,8 +74,8 @@ func (api *ConsensusAPI) ForkchoiceUpdatedWithWitnessV3(ctx context.Context, upd return engine.STATUS_INVALID, attributesErr("missing withdrawals") case params.BeaconRoot == nil: return engine.STATUS_INVALID, attributesErr("missing beacon root") - case !api.checkFork(params.Timestamp, forks.Cancun, forks.Prague, forks.Osaka, forks.BPO1, forks.BPO2, forks.BPO3, forks.BPO4, forks.BPO5): - return engine.STATUS_INVALID, unsupportedForkErr("fcuV3 must only be called for cancun/prague/osaka payloads") + case !api.checkFork(params.Timestamp, forks.Cancun, forks.Prague, forks.Osaka, forks.BPO1, forks.Amsterdam, forks.BPO2, forks.BPO3, forks.BPO4, forks.BPO5): + return engine.STATUS_INVALID, unsupportedForkErr("fcuV3 must only be called for cancun/prague/osaka/amsterdam payloads") } } // TODO(matt): the spec requires that fcu is applied when called on a valid @@ -162,6 +162,32 @@ func (api *ConsensusAPI) NewPayloadWithWitnessV4(ctx context.Context, params eng return api.newPayload(ctx, params, versionedHashes, beaconRoot, requests, true) } +// NewPayloadWithWitnessV5 is analogous to NewPayloadV5, only it also generates +// and returns a stateless witness after running the payload. +func (api *ConsensusAPI) NewPayloadWithWitnessV5(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, executionRequests []hexutil.Bytes) (engine.PayloadStatusV1, error) { + switch { + case params.Withdrawals == nil: + return invalidStatus, paramsErr("nil withdrawals post-shanghai") + case params.ExcessBlobGas == nil: + return invalidStatus, paramsErr("nil excessBlobGas post-cancun") + case params.BlobGasUsed == nil: + return invalidStatus, paramsErr("nil blobGasUsed post-cancun") + case versionedHashes == nil: + return invalidStatus, paramsErr("nil versionedHashes post-cancun") + case beaconRoot == nil: + return invalidStatus, paramsErr("nil beaconRoot post-cancun") + case executionRequests == nil: + return invalidStatus, paramsErr("nil executionRequests post-prague") + case !api.checkFork(params.Timestamp, forks.Amsterdam): + return invalidStatus, unsupportedForkErr("newPayloadWithWitnessV5 must only be called for amsterdam payloads") + } + requests := convertRequests(executionRequests) + if err := validateRequests(requests); err != nil { + return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(err) + } + return api.newPayload(params, versionedHashes, beaconRoot, requests, true) +} + // ExecuteStatelessPayloadV1 is analogous to NewPayloadV1, only it operates in // a stateless mode on top of a provided witness instead of the local database. func (api *ConsensusAPI) ExecuteStatelessPayloadV1(params engine.ExecutableData, opaqueWitness hexutil.Bytes) (engine.StatelessPayloadStatusV1, error) { @@ -239,6 +265,32 @@ func (api *ConsensusAPI) ExecuteStatelessPayloadV4(params engine.ExecutableData, return api.executeStatelessPayload(params, versionedHashes, beaconRoot, requests, opaqueWitness) } +// ExecuteStatelessPayloadV5 is analogous to NewPayloadV5, only it operates in +// a stateless mode on top of a provided witness instead of the local database. +func (api *ConsensusAPI) ExecuteStatelessPayloadV5(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, executionRequests []hexutil.Bytes, opaqueWitness hexutil.Bytes) (engine.StatelessPayloadStatusV1, error) { + switch { + case params.Withdrawals == nil: + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil withdrawals post-shanghai") + case params.ExcessBlobGas == nil: + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil excessBlobGas post-cancun") + case params.BlobGasUsed == nil: + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil blobGasUsed post-cancun") + case versionedHashes == nil: + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil versionedHashes post-cancun") + case beaconRoot == nil: + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil beaconRoot post-cancun") + case executionRequests == nil: + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, paramsErr("nil executionRequests post-prague") + case !api.checkFork(params.Timestamp, forks.Amsterdam): + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, unsupportedForkErr("executeStatelessPayloadV5 must only be called for amsterdam payloads") + } + requests := convertRequests(executionRequests) + if err := validateRequests(requests); err != nil { + return engine.StatelessPayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(err) + } + return api.executeStatelessPayload(params, versionedHashes, beaconRoot, requests, opaqueWitness) +} + func (api *ConsensusAPI) executeStatelessPayload(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte, opaqueWitness hexutil.Bytes) (engine.StatelessPayloadStatusV1, error) { log.Trace("Engine API request received", "method", "ExecuteStatelessPayload", "number", params.Number, "hash", params.BlockHash) block, err := engine.ExecutableDataToBlockNoHash(params, versionedHashes, beaconRoot, requests)