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 // https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#executionpayloadv4
// ExecutionPayloadV4 has the syntax of ExecutionPayloadV3 and appends the new // ExecutionPayloadV4 has the syntax of ExecutionPayloadV3 and appends the new
// field slotNumber. // fields slotNumber and blockAccessList (EIP-7928).
PayloadV4 PayloadVersion = 0x4 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") return engine.STATUS_INVALID, attributesErr("missing withdrawals")
case params.BeaconRoot == nil: case params.BeaconRoot == nil:
return engine.STATUS_INVALID, attributesErr("missing beacon root") 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): 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 payloads") 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 // 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 { } else {
// If the head block is already in our canonical chain, the beacon client is // If the head block is already in our canonical chain, the beacon client is
// probably resyncing. Ignore the update. // 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) 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 return valid(nil), nil
} }
@ -479,19 +485,23 @@ func (api *ConsensusAPI) GetPayloadV5(payloadID engine.PayloadID) (*engine.Execu
forks.BPO3, forks.BPO3,
forks.BPO4, forks.BPO4,
forks.BPO5, forks.BPO5,
forks.Amsterdam, // TODO (MariusVanDerWijden) remove
// Amsterdam uses GetPayloadV6
}) })
} }
// GetPayloadV6 returns a cached payload by id. This endpoint should only // GetPayloadV6 returns a cached payload by id. This endpoint should only
// be used after the Amsterdam fork. // 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) { func (api *ConsensusAPI) GetPayloadV6(payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) {
return api.getPayload( return api.getPayload(
payloadID, payloadID,
false, false,
[]engine.PayloadVersion{engine.PayloadV4}, []engine.PayloadVersion{engine.PayloadV4},
[]forks.Fork{ []forks.Fork{forks.Amsterdam},
forks.Amsterdam, )
})
} }
// getPayload will retrieve the specified payload and verify it conforms to the // 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") return invalidStatus, paramsErr("nil beaconRoot post-cancun")
case executionRequests == nil: case executionRequests == nil:
return invalidStatus, paramsErr("nil executionRequests post-prague") 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") return invalidStatus, unsupportedForkErr("newPayloadV4 must only be called for prague/osaka payloads")
} }
requests := convertRequests(executionRequests) requests := convertRequests(executionRequests)

View file

@ -74,8 +74,8 @@ func (api *ConsensusAPI) ForkchoiceUpdatedWithWitnessV3(ctx context.Context, upd
return engine.STATUS_INVALID, attributesErr("missing withdrawals") return engine.STATUS_INVALID, attributesErr("missing withdrawals")
case params.BeaconRoot == nil: case params.BeaconRoot == nil:
return engine.STATUS_INVALID, attributesErr("missing beacon root") 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): 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 payloads") 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 // 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) 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 // ExecuteStatelessPayloadV1 is analogous to NewPayloadV1, only it operates in
// a stateless mode on top of a provided witness instead of the local database. // 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) { 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) 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) { 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) log.Trace("Engine API request received", "method", "ExecuteStatelessPayload", "number", params.Number, "hash", params.BlockHash)
block, err := engine.ExecutableDataToBlockNoHash(params, versionedHashes, beaconRoot, requests) block, err := engine.ExecutableDataToBlockNoHash(params, versionedHashes, beaconRoot, requests)