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 <noreply@anthropic.com>
This commit is contained in:
Barnabas Busa 2026-01-26 15:21:19 +01:00 committed by MariusVanDerWijden
parent 29e0a6f404
commit ab98ca7599
3 changed files with 73 additions and 9 deletions

View file

@ -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
)

View file

@ -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)

View file

@ -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)