diff --git a/op-e2e/actions/l2_verifier.go b/op-e2e/actions/l2_verifier.go index 4fd260779..7130f943b 100644 --- a/op-e2e/actions/l2_verifier.go +++ b/op-e2e/actions/l2_verifier.go @@ -71,7 +71,7 @@ type safeDB interface { func NewL2Verifier(t Testing, log log.Logger, l1 derive.L1Fetcher, blobsSrc derive.L1BlobsFetcher, plasmaSrc driver.PlasmaIface, eng L2API, cfg *rollup.Config, syncCfg *sync.Config, safeHeadListener safeDB) *L2Verifier { metrics := &testutils.TestDerivationMetrics{} - engine := derive.NewEngineController(eng, log, metrics, cfg, syncCfg) + engine := derive.NewEngineController(eng, log, metrics, cfg, syncCfg, false) clSync := clsync.NewCLSync(log, cfg, metrics, engine) diff --git a/op-node/flags/flags.go b/op-node/flags/flags.go index 03c48f0f3..811e0fba2 100644 --- a/op-node/flags/flags.go +++ b/op-node/flags/flags.go @@ -251,6 +251,12 @@ var ( EnvVars: prefixEnvVars("SEQUENCER_PRIORITY"), Category: SequencerCategory, } + SequencerCombinedEngineFlag = &cli.BoolFlag{ + Name: "sequencer.combined-engine", + Usage: "Enable sequencer select combined engine api when sealing payload.", + EnvVars: prefixEnvVars("SEQUENCER_COMBINED_ENGINE"), + Category: SequencerCategory, + } SequencerL1Confs = &cli.Uint64Flag{ Name: "sequencer.l1-confs", Usage: "Number of L1 blocks to keep distance from the L1 head as a sequencer for picking an L1 origin.", @@ -437,6 +443,7 @@ var optionalFlags = []cli.Flag{ SequencerStoppedFlag, SequencerMaxSafeLagFlag, SequencerPriorityFlag, + SequencerCombinedEngineFlag, SequencerL1Confs, L1EpochPollIntervalFlag, RuntimeConfigReloadIntervalFlag, diff --git a/op-node/rollup/attributes/attributes_test.go b/op-node/rollup/attributes/attributes_test.go index 4f66b93fd..99510c485 100644 --- a/op-node/rollup/attributes/attributes_test.go +++ b/op-node/rollup/attributes/attributes_test.go @@ -181,7 +181,7 @@ func TestAttributesHandler(t *testing.T) { t.Run("drop stale attributes", func(t *testing.T) { logger := testlog.Logger(t, log.LevelInfo) eng := &testutils.MockEngine{} - ec := derive.NewEngineController(eng, logger, metrics.NoopMetrics, cfg, &sync.Config{SyncMode: sync.CLSync}) + ec := derive.NewEngineController(eng, logger, metrics.NoopMetrics, cfg, &sync.Config{SyncMode: sync.CLSync}, false) ah := NewAttributesHandler(logger, cfg, ec, eng) defer eng.AssertExpectations(t) @@ -195,7 +195,7 @@ func TestAttributesHandler(t *testing.T) { t.Run("pending gets reorged", func(t *testing.T) { logger := testlog.Logger(t, log.LevelInfo) eng := &testutils.MockEngine{} - ec := derive.NewEngineController(eng, logger, metrics.NoopMetrics, cfg, &sync.Config{SyncMode: sync.CLSync}) + ec := derive.NewEngineController(eng, logger, metrics.NoopMetrics, cfg, &sync.Config{SyncMode: sync.CLSync}, false) ah := NewAttributesHandler(logger, cfg, ec, eng) defer eng.AssertExpectations(t) @@ -210,7 +210,7 @@ func TestAttributesHandler(t *testing.T) { t.Run("consolidation fails", func(t *testing.T) { logger := testlog.Logger(t, log.LevelInfo) eng := &testutils.MockEngine{} - ec := derive.NewEngineController(eng, logger, metrics.NoopMetrics, cfg, &sync.Config{SyncMode: sync.CLSync}) + ec := derive.NewEngineController(eng, logger, metrics.NoopMetrics, cfg, &sync.Config{SyncMode: sync.CLSync}, false) ah := NewAttributesHandler(logger, cfg, ec, eng) ec.SetUnsafeHead(refA1) @@ -264,7 +264,7 @@ func TestAttributesHandler(t *testing.T) { fn := func(t *testing.T, lastInSpan bool) { logger := testlog.Logger(t, log.LevelInfo) eng := &testutils.MockEngine{} - ec := derive.NewEngineController(eng, logger, metrics.NoopMetrics, cfg, &sync.Config{SyncMode: sync.CLSync}) + ec := derive.NewEngineController(eng, logger, metrics.NoopMetrics, cfg, &sync.Config{SyncMode: sync.CLSync}, false) ah := NewAttributesHandler(logger, cfg, ec, eng) ec.SetUnsafeHead(refA1) @@ -323,7 +323,7 @@ func TestAttributesHandler(t *testing.T) { logger := testlog.Logger(t, log.LevelInfo) eng := &testutils.MockEngine{} - ec := derive.NewEngineController(eng, logger, metrics.NoopMetrics, cfg, &sync.Config{SyncMode: sync.CLSync}) + ec := derive.NewEngineController(eng, logger, metrics.NoopMetrics, cfg, &sync.Config{SyncMode: sync.CLSync}, false) ah := NewAttributesHandler(logger, cfg, ec, eng) ec.SetUnsafeHead(refA0) @@ -374,7 +374,7 @@ func TestAttributesHandler(t *testing.T) { logger := testlog.Logger(t, log.LevelInfo) eng := &testutils.MockEngine{} - ec := derive.NewEngineController(eng, logger, metrics.NoopMetrics, cfg, &sync.Config{SyncMode: sync.CLSync}) + ec := derive.NewEngineController(eng, logger, metrics.NoopMetrics, cfg, &sync.Config{SyncMode: sync.CLSync}, false) ah := NewAttributesHandler(logger, cfg, ec, eng) ec.SetUnsafeHead(refA0) @@ -398,7 +398,7 @@ func TestAttributesHandler(t *testing.T) { t.Run("no attributes", func(t *testing.T) { logger := testlog.Logger(t, log.LevelInfo) eng := &testutils.MockEngine{} - ec := derive.NewEngineController(eng, logger, metrics.NoopMetrics, cfg, &sync.Config{SyncMode: sync.CLSync}) + ec := derive.NewEngineController(eng, logger, metrics.NoopMetrics, cfg, &sync.Config{SyncMode: sync.CLSync}, false) ah := NewAttributesHandler(logger, cfg, ec, eng) defer eng.AssertExpectations(t) diff --git a/op-node/rollup/derive/engine_controller.go b/op-node/rollup/derive/engine_controller.go index f53ad7065..e0e8d2548 100644 --- a/op-node/rollup/derive/engine_controller.go +++ b/op-node/rollup/derive/engine_controller.go @@ -50,6 +50,7 @@ type ExecEngine interface { GetPayload(ctx context.Context, payloadInfo eth.PayloadInfo) (*eth.ExecutionPayloadEnvelope, error) ForkchoiceUpdate(ctx context.Context, state *eth.ForkchoiceState, attr *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) NewPayload(ctx context.Context, payload *eth.ExecutionPayload, parentBeaconBlockRoot *common.Hash) (*eth.PayloadStatusV1, error) + SealPayload(ctx context.Context, payloadInfo eth.PayloadInfo, fc *eth.ForkchoiceState, needPayload bool) (*eth.SealPayloadResponse, string, error) L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error) } @@ -84,9 +85,11 @@ type EngineController struct { buildingInfo eth.PayloadInfo buildingSafe bool safeAttrs *AttributesWithParent + + combinedAPI bool } -func NewEngineController(engine ExecEngine, log log.Logger, metrics Metrics, rollupCfg *rollup.Config, syncConfig *sync.Config) *EngineController { +func NewEngineController(engine ExecEngine, log log.Logger, metrics Metrics, rollupCfg *rollup.Config, syncConfig *sync.Config, combinedAPI bool) *EngineController { syncStatus := syncStatusCL if syncConfig.SyncMode == sync.ELSync { syncStatus = syncStatusWillStartEL @@ -102,6 +105,7 @@ func NewEngineController(engine ExecEngine, log log.Logger, metrics Metrics, rol elTriggerGap: syncConfig.ELTriggerGap, syncStatus: syncStatus, clock: clock.SystemClock, + combinedAPI: combinedAPI, } } @@ -267,7 +271,13 @@ func (e *EngineController) ConfirmPayload(ctx context.Context, agossip async.Asy } // Update the safe head if the payload is built with the last attributes in the batch. updateSafe := e.buildingSafe && e.safeAttrs != nil && e.safeAttrs.IsLastInSpan - envelope, errTyp, err := confirmPayload(ctx, e.log, e.engine, fc, e.buildingInfo, updateSafe, agossip, sequencerConductor, e.metrics) + + var envelope *eth.ExecutionPayloadEnvelope + if e.combinedAPI && !e.buildingSafe { + envelope, errTyp, err = confirmPayloadCombined(ctx, e.log, e.engine, fc, e.buildingInfo, updateSafe, agossip, sequencerConductor, e.metrics) + } else { + envelope, errTyp, err = confirmPayload(ctx, e.log, e.engine, fc, e.buildingInfo, updateSafe, agossip, sequencerConductor, e.metrics) + } if err != nil { return nil, errTyp, fmt.Errorf("failed to complete building on top of L2 chain %s, id: %s, error (%d): %w", e.buildingOnto, e.buildingInfo.ID, errTyp, err) } diff --git a/op-node/rollup/derive/engine_queue_test.go b/op-node/rollup/derive/engine_queue_test.go index 80e755aac..9b92e795e 100644 --- a/op-node/rollup/derive/engine_queue_test.go +++ b/op-node/rollup/derive/engine_queue_test.go @@ -291,7 +291,7 @@ func TestEngineQueue_ResetWhenUnsafeOriginNotCanonical(t *testing.T) { SyncMode: sync.CLSync, SkipSyncStartCheck: false, ELTriggerGap: 0, - }) + }, false) eq := NewEngineQueue(logger, cfg, eng, ec, metrics, prev, l1F, &sync.Config{}, safedb.Disabled, noopFinality{}, &fakeAttributesHandler{}) require.ErrorIs(t, eq.Reset(context.Background(), eth.L1BlockRef{}, eth.SystemConfig{}), io.EOF) @@ -634,7 +634,7 @@ func TestVerifyNewL1Origin(t *testing.T) { SyncMode: sync.CLSync, SkipSyncStartCheck: false, ELTriggerGap: 0, - }) + }, false) eq := NewEngineQueue(logger, cfg, eng, ec, metrics, prev, l1F, &sync.Config{}, safedb.Disabled, noopFinality{}, &fakeAttributesHandler{}) require.ErrorIs(t, eq.Reset(context.Background(), eth.L1BlockRef{}, eth.SystemConfig{}), io.EOF) @@ -738,7 +738,7 @@ func TestBlockBuildingRace(t *testing.T) { SyncMode: sync.CLSync, SkipSyncStartCheck: false, ELTriggerGap: 0, - }) + }, false) attribHandler := &fakeAttributesHandler{} eq := NewEngineQueue(logger, cfg, eng, ec, metrics, prev, l1F, &sync.Config{}, safedb.Disabled, noopFinality{}, attribHandler) require.ErrorIs(t, eq.Reset(context.Background(), eth.L1BlockRef{}, eth.SystemConfig{}), io.EOF) @@ -858,7 +858,7 @@ func TestResetLoop(t *testing.T) { SyncMode: sync.CLSync, SkipSyncStartCheck: false, ELTriggerGap: 0, - }) + }, false) eq := NewEngineQueue(logger, cfg, eng, ec, metrics.NoopMetrics, prev, l1F, &sync.Config{}, safedb.Disabled, noopFinality{}, &fakeAttributesHandler{}) eq.ec.SetUnsafeHead(refA2) eq.ec.SetSafeHead(refA1) diff --git a/op-node/rollup/derive/engine_update.go b/op-node/rollup/derive/engine_update.go index 30226d35d..656e52474 100644 --- a/op-node/rollup/derive/engine_update.go +++ b/op-node/rollup/derive/engine_update.go @@ -208,3 +208,131 @@ func confirmPayload( "txs", len(payload.Transactions), "update_safe", updateSafe) return envelope, BlockInsertOK, nil } + +// confirmPayloadCombined is equal to confirmPayload but using engine_opSealPayload API to combine GetPayload, NewPayload, ForckchoiceUpdated calls +func confirmPayloadCombined( + ctx context.Context, + log log.Logger, + eng ExecEngine, + fc eth.ForkchoiceState, + payloadInfo eth.PayloadInfo, + updateSafe bool, + agossip async.AsyncGossiper, + sequencerConductor conductor.SequencerConductor, + metrics Metrics, +) (out *eth.ExecutionPayloadEnvelope, errTyp BlockInsertionErrType, err error) { + start := time.Now() + type SealPayloadRet struct { + res *eth.SealPayloadResponse + errStage string + err error + } + sealPayloadRetCh := make(chan SealPayloadRet, 1) + go func() { + res, errStage, err := eng.SealPayload(ctx, payloadInfo, &fc, false) + sealPayloadRetCh <- SealPayloadRet{res, errStage, err} + }() + + type GetPayloadRet struct { + res *eth.ExecutionPayloadEnvelope + err error + } + getPayloadRetCh := make(chan GetPayloadRet, 1) + go func() { + res, err := eng.GetPayload(ctx, payloadInfo) + getPayloadRetCh <- GetPayloadRet{res, err} + }() + + getPayloadRet := <-getPayloadRetCh + envelope := getPayloadRet.res + getPayloadErr := getPayloadRet.err + validatePayloadErr := error(nil) + if getPayloadErr == nil { + payload := envelope.ExecutionPayload + validatePayloadErr = sanityCheckPayload(payload) + if validatePayloadErr == nil { + // TODO handle sequencerConductor component + if err := sequencerConductor.CommitUnsafePayload(ctx, envelope); err != nil { + log.Error("failed to commit unsafe payload to conductor", "payloadID", payloadInfo.ID, "err", err) + } + agossip.Gossip(envelope) + } + } + + sealPayloadRet := <-sealPayloadRetCh + sealRes := sealPayloadRet.res + errStage := sealPayloadRet.errStage + sealPayloadErr := sealPayloadRet.err + switch errStage { + case eth.GetPayloadStage: + return nil, BlockInsertTemporaryErr, fmt.Errorf("failed to get execution payload: %w", sealPayloadErr) + case eth.NewPayloadStage: + if sealPayloadErr != nil { + return nil, BlockInsertTemporaryErr, fmt.Errorf("failed to insert execution payload: %w", sealPayloadErr) + } + validationError := "validation error is nil" + if sealRes.PayloadStatus.ValidationError != nil { + validationError = *sealRes.PayloadStatus.ValidationError + } + if sealRes.PayloadStatus.Status == eth.ExecutionInvalid || sealRes.PayloadStatus.Status == eth.ExecutionInvalidBlockHash { + agossip.Clear() + log.Error("Seal payload failed to new payload", "payloadID", payloadInfo.ID, "status", sealRes.PayloadStatus) + return nil, BlockInsertPayloadErr, fmt.Errorf("failed to new payload, status: %s, validationError: %v", sealRes.PayloadStatus.Status, validationError) + } + if sealRes.PayloadStatus.Status != eth.ExecutionValid { + return nil, BlockInsertTemporaryErr, fmt.Errorf("failed to new payload, status: %s, validationError: %v", sealRes.PayloadStatus.Status, validationError) + } + case eth.ForkchoiceUpdatedStage: + if sealPayloadErr != nil { + var inputErr eth.InputError + if errors.As(sealPayloadErr, &inputErr) { + switch inputErr.Code { + case eth.InvalidForkchoiceState: + // if we succeed to update the forkchoice pre-payload, but fail post-payload, then it is a payload error + agossip.Clear() + return nil, BlockInsertPayloadErr, fmt.Errorf("post-block-creation forkchoice update was inconsistent with engine, need reset to resolve: %w", inputErr.Unwrap()) + default: + agossip.Clear() + return nil, BlockInsertPrestateErr, fmt.Errorf("unexpected error code in forkchoice-updated response: %w", sealPayloadErr) + } + } else { + agossip.Clear() + return nil, BlockInsertTemporaryErr, NewTemporaryError(fmt.Errorf("failed to make the new L2 block canonical via forkchoice: %w", sealPayloadErr)) + } + } + validationError := "validation error is nil" + if sealRes.PayloadStatus.ValidationError != nil { + validationError = *sealRes.PayloadStatus.ValidationError + } + if sealRes.PayloadStatus.Status != eth.ExecutionValid { + agossip.Clear() + return nil, BlockInsertPayloadErr, fmt.Errorf("failed to forkchoice update, status: %s, validationError: %v", sealRes.PayloadStatus.Status, validationError) + } + default: + if sealPayloadErr != nil { + return nil, BlockInsertTemporaryErr, NewTemporaryError(fmt.Errorf("failed to seal payload, err: %w", sealPayloadErr)) + } + if sealRes == nil { + return nil, BlockInsertTemporaryErr, NewTemporaryError(fmt.Errorf("failed to seal payload, got empty response")) + } + if sealRes.PayloadStatus.Status != eth.ExecutionValid { + return nil, BlockInsertTemporaryErr, NewTemporaryError(fmt.Errorf("failed to seal payload, status: %s", sealRes.PayloadStatus.Status)) + } + } + + if getPayloadErr != nil { + return nil, BlockInsertTemporaryErr, NewTemporaryError(fmt.Errorf("failed to get payload: %w", getPayloadErr)) + } + if validatePayloadErr != nil { + return nil, BlockInsertPayloadErr, NewCriticalError(fmt.Errorf("failed to validate payload but seal succeed: %w", validatePayloadErr)) + } + + agossip.Clear() + payload := envelope.ExecutionPayload + metrics.RecordSequencerStepTime("sealPayload", time.Since(start)) + log.Info("Sealed block succeed", "hash", payload.BlockHash, "number", uint64(payload.BlockNumber), + "state_root", payload.StateRoot, "timestamp", uint64(payload.Timestamp), "parent", payload.ParentHash, + "prev_randao", payload.PrevRandao, "fee_recipient", payload.FeeRecipient, + "txs", len(payload.Transactions), "update_safe", updateSafe) + return envelope, BlockInsertOK, nil +} diff --git a/op-node/rollup/driver/config.go b/op-node/rollup/driver/config.go index fa0d6932c..42ebe6f63 100644 --- a/op-node/rollup/driver/config.go +++ b/op-node/rollup/driver/config.go @@ -23,4 +23,6 @@ type Config struct { // SequencerPriority is true when sequencer step takes precedence over other steps. SequencerPriority bool `json:"sequencer_priority"` + + SequencerCombinedEngine bool `json:"sequencer_combined_engine"` } diff --git a/op-node/rollup/driver/driver.go b/op-node/rollup/driver/driver.go index f7f610f4f..7b127eccb 100644 --- a/op-node/rollup/driver/driver.go +++ b/op-node/rollup/driver/driver.go @@ -157,7 +157,7 @@ func NewDriver( sequencerConfDepth := NewConfDepth(driverCfg.SequencerConfDepth, l1State.L1Head, l1) findL1Origin := NewL1OriginSelector(log, cfg, sequencerConfDepth) verifConfDepth := NewConfDepth(driverCfg.VerifierConfDepth, l1State.L1Head, l1) - engine := derive.NewEngineController(l2, log, metrics, cfg, syncCfg) + engine := derive.NewEngineController(l2, log, metrics, cfg, syncCfg, driverCfg.SequencerCombinedEngine) clSync := clsync.NewCLSync(log, cfg, metrics, engine) var finalizer Finalizer diff --git a/op-node/rollup/types.go b/op-node/rollup/types.go index 6b1449949..3d3aa2075 100644 --- a/op-node/rollup/types.go +++ b/op-node/rollup/types.go @@ -508,6 +508,16 @@ func (c *Config) GetPayloadVersion(timestamp uint64) eth.EngineAPIMethod { } } +// SealPayloadVersion returns the EngineAPIMethod suitable for the chain hard fork version. +func (c *Config) SealPayloadVersion(timestamp uint64) eth.EngineAPIMethod { + if c.IsEcotone(timestamp) { + // Cancun + return eth.SealPayloadV3 + } else { + return eth.SealPayloadV2 + } +} + // GetOPPlasmaConfig validates and returns the plasma config from the rollup config. func (c *Config) GetOPPlasmaConfig() (plasma.Config, error) { if c.PlasmaConfig == nil { diff --git a/op-node/service.go b/op-node/service.go index c1fd6f8ab..134a916ee 100644 --- a/op-node/service.go +++ b/op-node/service.go @@ -204,12 +204,13 @@ func NewConfigPersistence(ctx *cli.Context) node.ConfigPersistence { func NewDriverConfig(ctx *cli.Context) *driver.Config { return &driver.Config{ - VerifierConfDepth: ctx.Uint64(flags.VerifierL1Confs.Name), - SequencerConfDepth: ctx.Uint64(flags.SequencerL1Confs.Name), - SequencerEnabled: ctx.Bool(flags.SequencerEnabledFlag.Name), - SequencerStopped: ctx.Bool(flags.SequencerStoppedFlag.Name), - SequencerMaxSafeLag: ctx.Uint64(flags.SequencerMaxSafeLagFlag.Name), - SequencerPriority: ctx.Bool(flags.SequencerPriorityFlag.Name), + VerifierConfDepth: ctx.Uint64(flags.VerifierL1Confs.Name), + SequencerConfDepth: ctx.Uint64(flags.SequencerL1Confs.Name), + SequencerEnabled: ctx.Bool(flags.SequencerEnabledFlag.Name), + SequencerStopped: ctx.Bool(flags.SequencerStoppedFlag.Name), + SequencerMaxSafeLag: ctx.Uint64(flags.SequencerMaxSafeLagFlag.Name), + SequencerPriority: ctx.Bool(flags.SequencerPriorityFlag.Name), + SequencerCombinedEngine: ctx.Bool(flags.SequencerCombinedEngineFlag.Name), } } diff --git a/op-program/client/driver/driver.go b/op-program/client/driver/driver.go index 46fcade40..eab8a41de 100644 --- a/op-program/client/driver/driver.go +++ b/op-program/client/driver/driver.go @@ -58,7 +58,7 @@ func NewDriver(logger log.Logger, cfg *rollup.Config, l1Source derive.L1Fetcher, SyncMode: sync.CLSync, SkipSyncStartCheck: false, ELTriggerGap: 0, - }) + }, false) attributesHandler := attributes.NewAttributesHandler(logger, cfg, engine, l2Source) pipeline := derive.NewDerivationPipeline(logger, cfg, l1Source, l1BlobsSource, plasma.Disabled, l2Source, engine, metrics.NoopMetrics, &sync.Config{}, safedb.Disabled, NoopFinalizer{}, attributesHandler) pipeline.Reset() diff --git a/op-program/client/l2/engine.go b/op-program/client/l2/engine.go index 34b8a5115..9dac9a782 100644 --- a/op-program/client/l2/engine.go +++ b/op-program/client/l2/engine.go @@ -89,6 +89,10 @@ func (o *OracleEngine) NewPayload(ctx context.Context, payload *eth.ExecutionPay } } +func (o *OracleEngine) SealPayload(ctx context.Context, payloadInfo eth.PayloadInfo, fc *eth.ForkchoiceState, needPayload bool) (*eth.SealPayloadResponse, string, error) { + return nil, "", nil +} + func (o *OracleEngine) PayloadByHash(ctx context.Context, hash common.Hash) (*eth.ExecutionPayloadEnvelope, error) { block := o.backend.GetBlockByHash(hash) if block == nil { diff --git a/op-service/eth/types.go b/op-service/eth/types.go index 605c10ffc..b191ceb0b 100644 --- a/op-service/eth/types.go +++ b/op-service/eth/types.go @@ -26,6 +26,12 @@ const ( InvalidPayloadAttributes ErrorCode = -38003 // Payload attributes are invalid / inconsistent. ) +const ( + GetPayloadStage = "getPayload" + NewPayloadStage = "newPayload" + ForkchoiceUpdatedStage = "forkchoiceUpdated" +) + var ErrBedrockScalarPaddingNotEmpty = errors.New("version 0 scalar value has non-empty padding") // InputError distinguishes an user-input error from regular rpc errors, @@ -367,6 +373,12 @@ type ForkchoiceUpdatedResult struct { PayloadID *PayloadID `json:"payloadId"` } +type SealPayloadResponse struct { + ErrStage string `json:"errStage"` + PayloadStatus PayloadStatusV1 `json:"payloadStatus"` + Payload *ExecutionPayloadEnvelope `json:"payload"` +} + // SystemConfig represents the rollup system configuration that carries over in every L2 block, // and may be changed through L1 system config events. // The initial SystemConfig at rollup genesis is embedded in the rollup configuration. @@ -512,4 +524,7 @@ const ( GetPayloadV2 EngineAPIMethod = "engine_getPayloadV2" GetPayloadV3 EngineAPIMethod = "engine_getPayloadV3" + + SealPayloadV2 EngineAPIMethod = "engine_opSealPayloadV2" + SealPayloadV3 EngineAPIMethod = "engine_opSealPayloadV3" ) diff --git a/op-service/sources/engine_client.go b/op-service/sources/engine_client.go index 9490df78f..68610ab26 100644 --- a/op-service/sources/engine_client.go +++ b/op-service/sources/engine_client.go @@ -61,6 +61,7 @@ type EngineVersionProvider interface { ForkchoiceUpdatedVersion(attr *eth.PayloadAttributes) eth.EngineAPIMethod NewPayloadVersion(timestamp uint64) eth.EngineAPIMethod GetPayloadVersion(timestamp uint64) eth.EngineAPIMethod + SealPayloadVersion(timestamp uint64) eth.EngineAPIMethod } func NewEngineAPIClient(rpc client.RPC, l log.Logger, evp EngineVersionProvider) *EngineAPIClient { @@ -178,6 +179,62 @@ func (s *EngineAPIClient) GetPayload(ctx context.Context, payloadInfo eth.Payloa return &result, nil } +// SealPayload is a combined call of GetPayload, NewPayload, ForkchoiceUpdated via engine_opSealPayload API +func (s *EngineAPIClient) SealPayload(ctx context.Context, payloadInfo eth.PayloadInfo, fc *eth.ForkchoiceState, needPayload bool) (*eth.SealPayloadResponse, string, error) { + e := s.log.New("payload_id", payloadInfo.ID) + e.Trace("sealing payload") + sCtx, sCancel := context.WithTimeout(ctx, time.Second*10) + defer sCancel() + var result eth.SealPayloadResponse + method := s.evp.SealPayloadVersion(payloadInfo.Timestamp) + err := s.RPC.CallContext(sCtx, &result, string(method), payloadInfo.ID, fc, needPayload) + if err != nil { + e.Error("Failed to seal payload", "payload_id", payloadInfo.ID, "err", err) + switch result.ErrStage { + case eth.GetPayloadStage: + if rpcErr, ok := err.(rpc.Error); ok { + code := eth.ErrorCode(rpcErr.ErrorCode()) + switch code { + case eth.UnknownPayload: + return nil, result.ErrStage, eth.InputError{ + Inner: err, + Code: code, + } + default: + return nil, result.ErrStage, fmt.Errorf("seal payload unrecognized rpc error: %w", err) + } + } + return nil, result.ErrStage, err + case eth.NewPayloadStage: + e.Error("Seal payload execution failed", "err", err) + if strings.Contains(err.Error(), derive.ErrELSyncTriggerUnexpected.Error()) { + result.PayloadStatus.Status = eth.ExecutionSyncing + return &result, result.ErrStage, err + } + return nil, result.ErrStage, fmt.Errorf("seal payload failed to execute payload: %w", err) + case eth.ForkchoiceUpdatedStage: + e.Error("Seal payload failed to share forkchoice-updated signal", "err", err) + if rpcErr, ok := err.(rpc.Error); ok { + code := eth.ErrorCode(rpcErr.ErrorCode()) + switch code { + case eth.InvalidForkchoiceState, eth.InvalidPayloadAttributes: + return nil, result.ErrStage, eth.InputError{ + Inner: err, + Code: code, + } + default: + return nil, result.ErrStage, fmt.Errorf("seal payload unrecognized rpc error: %w", err) + } + } + return nil, result.ErrStage, err + default: + return nil, result.ErrStage, err + } + } + e.Trace("Sealed payload") + return &result, result.ErrStage, nil +} + func (s *EngineAPIClient) SignalSuperchainV1(ctx context.Context, recommended, required params.ProtocolVersion) (params.ProtocolVersion, error) { var result params.ProtocolVersion err := s.RPC.CallContext(ctx, &result, "engine_signalSuperchainV1", &catalyst.SuperchainSignal{ diff --git a/op-service/testutils/mock_engine.go b/op-service/testutils/mock_engine.go index fa1f716d1..7d3351896 100644 --- a/op-service/testutils/mock_engine.go +++ b/op-service/testutils/mock_engine.go @@ -40,6 +40,15 @@ func (m *MockEngine) ExpectNewPayload(payload *eth.ExecutionPayload, parentBeaco m.Mock.On("NewPayload", mustJson(payload), mustJson(parentBeaconBlockRoot)).Once().Return(result, err) } +func (m *MockEngine) SealPayload(ctx context.Context, payloadInfo eth.PayloadInfo, fc *eth.ForkchoiceState, needPayload bool) (*eth.SealPayloadResponse, string, error) { + out := m.Mock.Called(payloadInfo.ID, fc, needPayload) + return out.Get(0).(*eth.SealPayloadResponse), out.Get(1).(string), out.Error(1) +} + +func (m *MockEngine) ExpectSealPayload(payloadInfo eth.PayloadInfo, fc *eth.ForkchoiceState, needPayload bool, result *eth.SealPayloadResponse, errStage string, err error) { + m.Mock.On("SealPayload", payloadInfo, fc, needPayload).Once().Return(result, errStage, err) +} + func (m *MockEngine) CachePayloadByHash(payload *eth.ExecutionPayloadEnvelope) bool { return true } diff --git a/op-wheel/engine/version_provider.go b/op-wheel/engine/version_provider.go index d3aaa377e..ae39d949c 100644 --- a/op-wheel/engine/version_provider.go +++ b/op-wheel/engine/version_provider.go @@ -42,3 +42,14 @@ func (v StaticVersionProvider) GetPayloadVersion(uint64) eth.EngineAPIMethod { panic("invalid Engine API version: " + strconv.Itoa(int(v))) } } + +func (v StaticVersionProvider) SealPayloadVersion(uint64) eth.EngineAPIMethod { + switch int(v) { + case 1, 2: + return eth.SealPayloadV2 + case 3: + return eth.SealPayloadV3 + default: + panic("invalid Engine API version: " + strconv.Itoa(int(v))) + } +}