Commit f7920585 authored by Sebastian Stammler's avatar Sebastian Stammler Committed by GitHub

op-wheel: Support for Engine API V3 calls & version selection (#9681)

* op-service: split EngineAPIClient out of EngineClient

This way, the Engine API RPC client can be reused in other components, like op-wheel.

* op-service/eth: Add BlockAsPayloadEnv

* op-wheel: Use new Engine API V3 calls

* op-wheel: Add rewind command

Also adds an open RCP endpoint because the "debug" rpc namespace is not
available on the authenticated endpoint.

This also fixes chain config loading.

Also sets some sane default http RPC endpoints.

* op-wheel: Move engine.version validation into flag Action

* op-wheel: improve rewind command
parent 89747141
...@@ -94,14 +94,7 @@ func (o *OracleEngine) PayloadByHash(ctx context.Context, hash common.Hash) (*et ...@@ -94,14 +94,7 @@ func (o *OracleEngine) PayloadByHash(ctx context.Context, hash common.Hash) (*et
if block == nil { if block == nil {
return nil, ErrNotFound return nil, ErrNotFound
} }
payload, err := eth.BlockAsPayload(block, o.rollupCfg.CanyonTime) return eth.BlockAsPayloadEnv(block, o.rollupCfg.CanyonTime)
if err != nil {
return nil, err
}
return &eth.ExecutionPayloadEnvelope{
ParentBeaconBlockRoot: block.BeaconRoot(),
ExecutionPayload: payload,
}, nil
} }
func (o *OracleEngine) PayloadByNumber(ctx context.Context, n uint64) (*eth.ExecutionPayloadEnvelope, error) { func (o *OracleEngine) PayloadByNumber(ctx context.Context, n uint64) (*eth.ExecutionPayloadEnvelope, error) {
......
...@@ -316,12 +316,7 @@ func (ea *L2EngineAPI) getPayload(ctx context.Context, payloadId eth.PayloadID) ...@@ -316,12 +316,7 @@ func (ea *L2EngineAPI) getPayload(ctx context.Context, payloadId eth.PayloadID)
return nil, engine.UnknownPayload return nil, engine.UnknownPayload
} }
payload, err := eth.BlockAsPayload(bl, ea.config().CanyonTime) return eth.BlockAsPayloadEnv(bl, ea.config().CanyonTime)
if err != nil {
return nil, err
}
return &eth.ExecutionPayloadEnvelope{ExecutionPayload: payload, ParentBeaconBlockRoot: bl.BeaconRoot()}, nil
} }
func (ea *L2EngineAPI) forkchoiceUpdated(ctx context.Context, state *eth.ForkchoiceState, attr *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) { func (ea *L2EngineAPI) forkchoiceUpdated(ctx context.Context, state *eth.ForkchoiceState, attr *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) {
......
...@@ -152,11 +152,13 @@ type Uint256Quantity = hexutil.U256 ...@@ -152,11 +152,13 @@ type Uint256Quantity = hexutil.U256
type Data = hexutil.Bytes type Data = hexutil.Bytes
type PayloadID = engine.PayloadID type (
type PayloadInfo struct { PayloadID = engine.PayloadID
PayloadInfo struct {
ID PayloadID ID PayloadID
Timestamp uint64 Timestamp uint64
} }
)
type ExecutionPayloadEnvelope struct { type ExecutionPayloadEnvelope struct {
ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot,omitempty"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot,omitempty"`
...@@ -287,6 +289,17 @@ func BlockAsPayload(bl *types.Block, canyonForkTime *uint64) (*ExecutionPayload, ...@@ -287,6 +289,17 @@ func BlockAsPayload(bl *types.Block, canyonForkTime *uint64) (*ExecutionPayload,
return payload, nil return payload, nil
} }
func BlockAsPayloadEnv(bl *types.Block, canyonForkTime *uint64) (*ExecutionPayloadEnvelope, error) {
payload, err := BlockAsPayload(bl, canyonForkTime)
if err != nil {
return nil, err
}
return &ExecutionPayloadEnvelope{
ExecutionPayload: payload,
ParentBeaconBlockRoot: bl.BeaconRoot(),
}, nil
}
type PayloadAttributes struct { type PayloadAttributes struct {
// value for the timestamp field of the new payload // value for the timestamp field of the new payload
Timestamp Uint64Quantity `json:"timestamp"` Timestamp Uint64Quantity `json:"timestamp"`
...@@ -296,14 +309,17 @@ type PayloadAttributes struct { ...@@ -296,14 +309,17 @@ type PayloadAttributes struct {
SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient"` SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient"`
// Withdrawals to include into the block -- should be nil or empty depending on Shanghai enablement // Withdrawals to include into the block -- should be nil or empty depending on Shanghai enablement
Withdrawals *types.Withdrawals `json:"withdrawals,omitempty"` Withdrawals *types.Withdrawals `json:"withdrawals,omitempty"`
// parentBeaconBlockRoot optional extension in Dencun
ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot,omitempty"`
// Optimism additions
// Transactions to force into the block (always at the start of the transactions list). // Transactions to force into the block (always at the start of the transactions list).
Transactions []Data `json:"transactions,omitempty"` Transactions []Data `json:"transactions,omitempty"`
// NoTxPool to disable adding any transactions from the transaction-pool. // NoTxPool to disable adding any transactions from the transaction-pool.
NoTxPool bool `json:"noTxPool,omitempty"` NoTxPool bool `json:"noTxPool,omitempty"`
// GasLimit override // GasLimit override
GasLimit *Uint64Quantity `json:"gasLimit,omitempty"` GasLimit *Uint64Quantity `json:"gasLimit,omitempty"`
// parentBeaconBlockRoot optional extension in Dencun
ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot,omitempty"`
} }
type ExecutePayloadStatus string type ExecutePayloadStatus string
......
...@@ -31,6 +31,7 @@ func EngineClientDefaultConfig(config *rollup.Config) *EngineClientConfig { ...@@ -31,6 +31,7 @@ func EngineClientDefaultConfig(config *rollup.Config) *EngineClientConfig {
// EngineClient extends L2Client with engine API bindings. // EngineClient extends L2Client with engine API bindings.
type EngineClient struct { type EngineClient struct {
*L2Client *L2Client
*EngineAPIClient
} }
func NewEngineClient(client client.RPC, log log.Logger, metrics caching.Metrics, config *EngineClientConfig) (*EngineClient, error) { func NewEngineClient(client client.RPC, log log.Logger, metrics caching.Metrics, config *EngineClientConfig) (*EngineClient, error) {
...@@ -39,11 +40,39 @@ func NewEngineClient(client client.RPC, log log.Logger, metrics caching.Metrics, ...@@ -39,11 +40,39 @@ func NewEngineClient(client client.RPC, log log.Logger, metrics caching.Metrics,
return nil, err return nil, err
} }
engineAPIClient := NewEngineAPIClient(client, log, config.RollupCfg)
return &EngineClient{ return &EngineClient{
L2Client: l2Client, L2Client: l2Client,
EngineAPIClient: engineAPIClient,
}, nil }, nil
} }
// EngineAPIClient is an RPC client for the Engine API functions.
type EngineAPIClient struct {
RPC client.RPC
log log.Logger
evp EngineVersionProvider
}
type EngineVersionProvider interface {
ForkchoiceUpdatedVersion(attr *eth.PayloadAttributes) eth.EngineAPIMethod
NewPayloadVersion(timestamp uint64) eth.EngineAPIMethod
GetPayloadVersion(timestamp uint64) eth.EngineAPIMethod
}
func NewEngineAPIClient(rpc client.RPC, l log.Logger, evp EngineVersionProvider) *EngineAPIClient {
return &EngineAPIClient{
RPC: rpc,
log: l,
evp: evp,
}
}
// EngineVersionProvider returns the underlying engine version provider used for
// resolving the correct Engine API versions.
func (s *EngineAPIClient) EngineVersionProvider() EngineVersionProvider { return s.evp }
// ForkchoiceUpdate updates the forkchoice on the execution client. If attributes is not nil, the engine client will also begin building a block // ForkchoiceUpdate updates the forkchoice on the execution client. If attributes is not nil, the engine client will also begin building a block
// based on attributes after the new head block and return the payload ID. // based on attributes after the new head block and return the payload ID.
// //
...@@ -51,15 +80,15 @@ func NewEngineClient(client client.RPC, log log.Logger, metrics caching.Metrics, ...@@ -51,15 +80,15 @@ func NewEngineClient(client client.RPC, log log.Logger, metrics caching.Metrics,
// 1. Processing error: ForkchoiceUpdatedResult.PayloadStatusV1.ValidationError or other non-success PayloadStatusV1, // 1. Processing error: ForkchoiceUpdatedResult.PayloadStatusV1.ValidationError or other non-success PayloadStatusV1,
// 2. `error` as eth.InputError: the forkchoice state or attributes are not valid. // 2. `error` as eth.InputError: the forkchoice state or attributes are not valid.
// 3. Other types of `error`: temporary RPC errors, like timeouts. // 3. Other types of `error`: temporary RPC errors, like timeouts.
func (s *EngineClient) ForkchoiceUpdate(ctx context.Context, fc *eth.ForkchoiceState, attributes *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) { func (s *EngineAPIClient) ForkchoiceUpdate(ctx context.Context, fc *eth.ForkchoiceState, attributes *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) {
llog := s.log.New("state", fc) // local logger llog := s.log.New("state", fc) // local logger
tlog := llog.New("attr", attributes) // trace logger tlog := llog.New("attr", attributes) // trace logger
tlog.Trace("Sharing forkchoice-updated signal") tlog.Trace("Sharing forkchoice-updated signal")
fcCtx, cancel := context.WithTimeout(ctx, time.Second*5) fcCtx, cancel := context.WithTimeout(ctx, time.Second*5)
defer cancel() defer cancel()
var result eth.ForkchoiceUpdatedResult var result eth.ForkchoiceUpdatedResult
method := s.rollupCfg.ForkchoiceUpdatedVersion(attributes) method := s.evp.ForkchoiceUpdatedVersion(attributes)
err := s.client.CallContext(fcCtx, &result, string(method), fc, attributes) err := s.RPC.CallContext(fcCtx, &result, string(method), fc, attributes)
if err == nil { if err == nil {
tlog.Trace("Shared forkchoice-updated signal") tlog.Trace("Shared forkchoice-updated signal")
if attributes != nil { // block building is optional, we only get a payload ID if we are building a block if attributes != nil { // block building is optional, we only get a payload ID if we are building a block
...@@ -87,7 +116,7 @@ func (s *EngineClient) ForkchoiceUpdate(ctx context.Context, fc *eth.ForkchoiceS ...@@ -87,7 +116,7 @@ func (s *EngineClient) ForkchoiceUpdate(ctx context.Context, fc *eth.ForkchoiceS
// NewPayload executes a full block on the execution engine. // NewPayload executes a full block on the execution engine.
// This returns a PayloadStatusV1 which encodes any validation/processing error, // This returns a PayloadStatusV1 which encodes any validation/processing error,
// and this type of error is kept separate from the returned `error` used for RPC errors, like timeouts. // and this type of error is kept separate from the returned `error` used for RPC errors, like timeouts.
func (s *EngineClient) NewPayload(ctx context.Context, payload *eth.ExecutionPayload, parentBeaconBlockRoot *common.Hash) (*eth.PayloadStatusV1, error) { func (s *EngineAPIClient) NewPayload(ctx context.Context, payload *eth.ExecutionPayload, parentBeaconBlockRoot *common.Hash) (*eth.PayloadStatusV1, error) {
e := s.log.New("block_hash", payload.BlockHash) e := s.log.New("block_hash", payload.BlockHash)
e.Trace("sending payload for execution") e.Trace("sending payload for execution")
...@@ -96,11 +125,11 @@ func (s *EngineClient) NewPayload(ctx context.Context, payload *eth.ExecutionPay ...@@ -96,11 +125,11 @@ func (s *EngineClient) NewPayload(ctx context.Context, payload *eth.ExecutionPay
var result eth.PayloadStatusV1 var result eth.PayloadStatusV1
var err error var err error
switch method := s.rollupCfg.NewPayloadVersion(uint64(payload.Timestamp)); method { switch method := s.evp.NewPayloadVersion(uint64(payload.Timestamp)); method {
case eth.NewPayloadV3: case eth.NewPayloadV3:
err = s.client.CallContext(execCtx, &result, string(method), payload, []common.Hash{}, parentBeaconBlockRoot) err = s.RPC.CallContext(execCtx, &result, string(method), payload, []common.Hash{}, parentBeaconBlockRoot)
case eth.NewPayloadV2: case eth.NewPayloadV2:
err = s.client.CallContext(execCtx, &result, string(method), payload) err = s.RPC.CallContext(execCtx, &result, string(method), payload)
default: default:
return nil, fmt.Errorf("unsupported NewPayload version: %s", method) return nil, fmt.Errorf("unsupported NewPayload version: %s", method)
} }
...@@ -117,12 +146,12 @@ func (s *EngineClient) NewPayload(ctx context.Context, payload *eth.ExecutionPay ...@@ -117,12 +146,12 @@ func (s *EngineClient) NewPayload(ctx context.Context, payload *eth.ExecutionPay
// There may be two types of error: // There may be two types of error:
// 1. `error` as eth.InputError: the payload ID may be unknown // 1. `error` as eth.InputError: the payload ID may be unknown
// 2. Other types of `error`: temporary RPC errors, like timeouts. // 2. Other types of `error`: temporary RPC errors, like timeouts.
func (s *EngineClient) GetPayload(ctx context.Context, payloadInfo eth.PayloadInfo) (*eth.ExecutionPayloadEnvelope, error) { func (s *EngineAPIClient) GetPayload(ctx context.Context, payloadInfo eth.PayloadInfo) (*eth.ExecutionPayloadEnvelope, error) {
e := s.log.New("payload_id", payloadInfo.ID) e := s.log.New("payload_id", payloadInfo.ID)
e.Trace("getting payload") e.Trace("getting payload")
var result eth.ExecutionPayloadEnvelope var result eth.ExecutionPayloadEnvelope
method := s.rollupCfg.GetPayloadVersion(payloadInfo.Timestamp) method := s.evp.GetPayloadVersion(payloadInfo.Timestamp)
err := s.client.CallContext(ctx, &result, string(method), payloadInfo.ID) err := s.RPC.CallContext(ctx, &result, string(method), payloadInfo.ID)
if err != nil { if err != nil {
e.Warn("Failed to get payload", "payload_id", payloadInfo.ID, "err", err) e.Warn("Failed to get payload", "payload_id", payloadInfo.ID, "err", err)
if rpcErr, ok := err.(rpc.Error); ok { if rpcErr, ok := err.(rpc.Error); ok {
...@@ -143,9 +172,9 @@ func (s *EngineClient) GetPayload(ctx context.Context, payloadInfo eth.PayloadIn ...@@ -143,9 +172,9 @@ func (s *EngineClient) GetPayload(ctx context.Context, payloadInfo eth.PayloadIn
return &result, nil return &result, nil
} }
func (s *EngineClient) SignalSuperchainV1(ctx context.Context, recommended, required params.ProtocolVersion) (params.ProtocolVersion, error) { func (s *EngineAPIClient) SignalSuperchainV1(ctx context.Context, recommended, required params.ProtocolVersion) (params.ProtocolVersion, error) {
var result params.ProtocolVersion var result params.ProtocolVersion
err := s.client.CallContext(ctx, &result, "engine_signalSuperchainV1", &catalyst.SuperchainSignal{ err := s.RPC.CallContext(ctx, &result, "engine_signalSuperchainV1", &catalyst.SuperchainSignal{
Recommended: recommended, Recommended: recommended,
Required: required, Required: required,
}) })
......
...@@ -18,12 +18,16 @@ import ( ...@@ -18,12 +18,16 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum-optimism/optimism/op-node/rollup"
opservice "github.com/ethereum-optimism/optimism/op-service" opservice "github.com/ethereum-optimism/optimism/op-service"
"github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum-optimism/optimism/op-service/client"
oplog "github.com/ethereum-optimism/optimism/op-service/log" oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics" opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum-optimism/optimism/op-service/sources"
"github.com/ethereum-optimism/optimism/op-wheel/cheat" "github.com/ethereum-optimism/optimism/op-wheel/cheat"
"github.com/ethereum-optimism/optimism/op-wheel/engine" "github.com/ethereum-optimism/optimism/op-wheel/engine"
) )
...@@ -50,8 +54,9 @@ var ( ...@@ -50,8 +54,9 @@ var (
} }
EngineEndpoint = &cli.StringFlag{ EngineEndpoint = &cli.StringFlag{
Name: "engine", Name: "engine",
Usage: "Engine API RPC endpoint, can be HTTP/WS/IPC", Usage: "Authenticated Engine API RPC endpoint, can be HTTP/WS/IPC",
Required: true, Required: true,
Value: "http://localhost:8551/",
EnvVars: prefixEnvVars("ENGINE"), EnvVars: prefixEnvVars("ENGINE"),
} }
EngineJWTPath = &cli.StringFlag{ EngineJWTPath = &cli.StringFlag{
...@@ -61,6 +66,23 @@ var ( ...@@ -61,6 +66,23 @@ var (
TakesFile: true, TakesFile: true,
EnvVars: prefixEnvVars("ENGINE_JWT_SECRET"), EnvVars: prefixEnvVars("ENGINE_JWT_SECRET"),
} }
EngineOpenEndpoint = &cli.StringFlag{
Name: "engine.open",
Usage: "Open Engine API RPC endpoint, can be HTTP/WS/IPC",
Value: "http://localhost:8545/",
EnvVars: prefixEnvVars("ENGINE_OPEN"),
}
EngineVersion = &cli.IntFlag{
Name: "engine.version",
Usage: "Engine API version to use for Engine calls (1, 2, or 3)",
EnvVars: prefixEnvVars("ENGINE_VERSION"),
Action: func(ctx *cli.Context, ev int) error {
if ev < 1 || ev > 3 {
return fmt.Errorf("invalid Engine API version: %d", ev)
}
return nil
},
}
FeeRecipientFlag = &cli.GenericFlag{ FeeRecipientFlag = &cli.GenericFlag{
Name: "fee-recipient", Name: "fee-recipient",
Usage: "fee-recipient of the block building", Usage: "fee-recipient of the block building",
...@@ -92,6 +114,12 @@ var ( ...@@ -92,6 +114,12 @@ var (
} }
) )
func withEngineFlags(flags ...cli.Flag) []cli.Flag {
return append(append(flags,
EngineEndpoint, EngineJWTPath, EngineOpenEndpoint, EngineVersion),
oplog.CLIFlags(envVarPrefix)...)
}
func ParseBuildingArgs(ctx *cli.Context) *engine.BlockBuildingSettings { func ParseBuildingArgs(ctx *cli.Context) *engine.BlockBuildingSettings {
return &engine.BlockBuildingSettings{ return &engine.BlockBuildingSettings{
BlockTime: ctx.Uint64(BlockTimeFlag.Name), BlockTime: ctx.Uint64(BlockTimeFlag.Name),
...@@ -124,19 +152,84 @@ func CheatRawDBAction(readOnly bool, fn func(ctx *cli.Context, db ethdb.Database ...@@ -124,19 +152,84 @@ func CheatRawDBAction(readOnly bool, fn func(ctx *cli.Context, db ethdb.Database
} }
} }
func EngineAction(fn func(ctx *cli.Context, client client.RPC) error) cli.ActionFunc { func EngineAction(fn func(ctx *cli.Context, client *sources.EngineAPIClient, lgr log.Logger) error) cli.ActionFunc {
return func(ctx *cli.Context) error { return func(ctx *cli.Context) error {
lgr := initLogger(ctx)
rpc, err := initEngineRPC(ctx, lgr)
if err != nil {
return fmt.Errorf("failed to dial Engine API endpoint %q: %w",
ctx.String(EngineEndpoint.Name), err)
}
evp, err := initVersionProvider(ctx, lgr)
if err != nil {
return fmt.Errorf("failed to init Engine version provider: %w", err)
}
client := sources.NewEngineAPIClient(rpc, lgr, evp)
return fn(ctx, client, lgr)
}
}
func initLogger(ctx *cli.Context) log.Logger {
logCfg := oplog.ReadCLIConfig(ctx)
lgr := oplog.NewLogger(oplog.AppOut(ctx), logCfg)
oplog.SetGlobalLogHandler(lgr.Handler())
return lgr
}
func initEngineRPC(ctx *cli.Context, lgr log.Logger) (client.RPC, error) {
jwtData, err := os.ReadFile(ctx.String(EngineJWTPath.Name)) jwtData, err := os.ReadFile(ctx.String(EngineJWTPath.Name))
if err != nil { if err != nil {
return fmt.Errorf("failed to read jwt: %w", err) return nil, fmt.Errorf("failed to read jwt: %w", err)
} }
secret := common.HexToHash(strings.TrimSpace(string(jwtData))) secret := common.HexToHash(strings.TrimSpace(string(jwtData)))
endpoint := ctx.String(EngineEndpoint.Name) endpoint := ctx.String(EngineEndpoint.Name)
client, err := engine.DialClient(context.Background(), endpoint, secret) return client.NewRPC(ctx.Context, lgr, endpoint,
client.WithGethRPCOptions(rpc.WithHTTPAuth(node.NewJWTAuth(secret))))
}
func initVersionProvider(ctx *cli.Context, lgr log.Logger) (sources.EngineVersionProvider, error) {
// static configuration takes precedent, if set
if ctx.IsSet(EngineVersion.Name) {
ev := ctx.Int(EngineVersion.Name)
return engine.StaticVersionProvider(ev), nil
}
// otherwise get config from EL
rpc, err := initOpenEngineRPC(ctx, lgr)
if err != nil {
return nil, err
}
cfg, err := engine.GetChainConfig(ctx.Context, rpc)
if err != nil { if err != nil {
return fmt.Errorf("failed to dial Engine API endpoint %q: %w", endpoint, err) return nil, err
} }
return fn(ctx, client) return rollupFromGethConfig(cfg), nil
}
func initOpenEngineRPC(ctx *cli.Context, lgr log.Logger) (client.RPC, error) {
openEP := ctx.String(EngineOpenEndpoint.Name)
rpc, err := client.NewRPC(ctx.Context, lgr, openEP)
if err != nil {
return nil, fmt.Errorf("failed to dial open Engine endpoint %q: %w", openEP, err)
}
return rpc, nil
}
// rollupFromGethConfig returns a very incomplete rollup config with only the
// L2ChainID and (most) fork activation timestamps set.
//
// Because Delta was a pure CL fork, its time isn't set either.
//
// This incomplete [rollup.Config] can be used as a [sources.EngineVersionProvider].
func rollupFromGethConfig(cfg *params.ChainConfig) *rollup.Config {
return &rollup.Config{
L2ChainID: cfg.ChainID,
RegolithTime: cfg.RegolithTime,
CanyonTime: cfg.CanyonTime,
EcotoneTime: cfg.EcotoneTime,
InteropTime: cfg.InteropTime,
} }
} }
...@@ -374,41 +467,35 @@ var ( ...@@ -374,41 +467,35 @@ var (
EngineBlockCmd = &cli.Command{ EngineBlockCmd = &cli.Command{
Name: "block", Name: "block",
Usage: "build the next block using the Engine API", Usage: "build the next block using the Engine API",
Flags: []cli.Flag{ Flags: withEngineFlags(
EngineEndpoint, EngineJWTPath,
FeeRecipientFlag, RandaoFlag, BlockTimeFlag, BuildingTime, AllowGaps, FeeRecipientFlag, RandaoFlag, BlockTimeFlag, BuildingTime, AllowGaps,
}, ),
// TODO: maybe support transaction and tx pool engine flags, since we use op-geth? // TODO: maybe support transaction and tx pool engine flags, since we use op-geth?
// TODO: reorg flag // TODO: reorg flag
// TODO: finalize/safe flag // TODO: finalize/safe flag
Action: EngineAction(func(ctx *cli.Context, client client.RPC) error { Action: EngineAction(func(ctx *cli.Context, client *sources.EngineAPIClient, _ log.Logger) error {
settings := ParseBuildingArgs(ctx) settings := ParseBuildingArgs(ctx)
status, err := engine.Status(context.Background(), client) status, err := engine.Status(context.Background(), client.RPC)
if err != nil { if err != nil {
return err return err
} }
payload, err := engine.BuildBlock(context.Background(), client, status, settings) payloadEnv, err := engine.BuildBlock(context.Background(), client, status, settings)
if err != nil { if err != nil {
return err return err
} }
_, err = io.WriteString(ctx.App.Writer, payload.BlockHash.String()) fmt.Fprintln(ctx.App.Writer, payloadEnv.ExecutionPayload.BlockHash)
return err return nil
}), }),
} }
EngineAutoCmd = &cli.Command{ EngineAutoCmd = &cli.Command{
Name: "auto", Name: "auto",
Usage: "Run a proof-of-nothing chain with fixed block time.", Usage: "Run a proof-of-nothing chain with fixed block time.",
Description: "The block time can be changed. The execution engine must be synced to a post-Merge state first.", Description: "The block time can be changed. The execution engine must be synced to a post-Merge state first.",
Flags: append(append([]cli.Flag{ Flags: append(withEngineFlags(
EngineEndpoint, EngineJWTPath, FeeRecipientFlag, RandaoFlag, BlockTimeFlag, BuildingTime, AllowGaps),
FeeRecipientFlag, RandaoFlag, BlockTimeFlag, BuildingTime, AllowGaps, opmetrics.CLIFlags(envVarPrefix)...),
}, oplog.CLIFlags(envVarPrefix)...), opmetrics.CLIFlags(envVarPrefix)...), Action: EngineAction(func(ctx *cli.Context, client *sources.EngineAPIClient, l log.Logger) error {
Action: EngineAction(func(ctx *cli.Context, client client.RPC) error {
logCfg := oplog.ReadCLIConfig(ctx)
l := oplog.NewLogger(oplog.AppOut(ctx), logCfg)
oplog.SetGlobalLogHandler(l.Handler())
settings := ParseBuildingArgs(ctx) settings := ParseBuildingArgs(ctx)
// TODO: finalize/safe flag // TODO: finalize/safe flag
...@@ -435,9 +522,9 @@ var ( ...@@ -435,9 +522,9 @@ var (
} }
EngineStatusCmd = &cli.Command{ EngineStatusCmd = &cli.Command{
Name: "status", Name: "status",
Flags: []cli.Flag{EngineEndpoint, EngineJWTPath}, Flags: withEngineFlags(),
Action: EngineAction(func(ctx *cli.Context, client client.RPC) error { Action: EngineAction(func(ctx *cli.Context, client *sources.EngineAPIClient, _ log.Logger) error {
stat, err := engine.Status(context.Background(), client) stat, err := engine.Status(context.Background(), client.RPC)
if err != nil { if err != nil {
return err return err
} }
...@@ -448,16 +535,15 @@ var ( ...@@ -448,16 +535,15 @@ var (
} }
EngineCopyCmd = &cli.Command{ EngineCopyCmd = &cli.Command{
Name: "copy", Name: "copy",
Flags: []cli.Flag{ Flags: withEngineFlags(
EngineEndpoint, EngineJWTPath,
&cli.StringFlag{ &cli.StringFlag{
Name: "source", Name: "source",
Usage: "Unauthenticated regular eth JSON RPC to pull block data from, can be HTTP/WS/IPC.", Usage: "Unauthenticated regular eth JSON RPC to pull block data from, can be HTTP/WS/IPC.",
Required: true, Required: true,
EnvVars: prefixEnvVars("ENGINE"), EnvVars: prefixEnvVars("SOURCE"),
}, },
}, ),
Action: EngineAction(func(ctx *cli.Context, dest client.RPC) error { Action: EngineAction(func(ctx *cli.Context, dest *sources.EngineAPIClient, _ log.Logger) error {
rpcClient, err := rpc.DialOptions(context.Background(), ctx.String("source")) rpcClient, err := rpc.DialOptions(context.Background(), ctx.String("source"))
if err != nil { if err != nil {
return fmt.Errorf("failed to dial engine source endpoint: %w", err) return fmt.Errorf("failed to dial engine source endpoint: %w", err)
...@@ -470,13 +556,12 @@ var ( ...@@ -470,13 +556,12 @@ var (
EngineCopyPayloadCmd = &cli.Command{ EngineCopyPayloadCmd = &cli.Command{
Name: "copy-payload", Name: "copy-payload",
Description: "Take the block by number from source and insert it to the engine with NewPayload. No other calls are made.", Description: "Take the block by number from source and insert it to the engine with NewPayload. No other calls are made.",
Flags: []cli.Flag{ Flags: withEngineFlags(
EngineEndpoint, EngineJWTPath,
&cli.StringFlag{ &cli.StringFlag{
Name: "source", Name: "source",
Usage: "Unauthenticated regular eth JSON RPC to pull block data from, can be HTTP/WS/IPC.", Usage: "Unauthenticated regular eth JSON RPC to pull block data from, can be HTTP/WS/IPC.",
Required: true, Required: true,
EnvVars: prefixEnvVars("ENGINE"), EnvVars: prefixEnvVars("SOURCE"),
}, },
&cli.Uint64Flag{ &cli.Uint64Flag{
Name: "number", Name: "number",
...@@ -484,8 +569,8 @@ var ( ...@@ -484,8 +569,8 @@ var (
Required: true, Required: true,
EnvVars: prefixEnvVars("NUMBER"), EnvVars: prefixEnvVars("NUMBER"),
}, },
}, ),
Action: EngineAction(func(ctx *cli.Context, dest client.RPC) error { Action: EngineAction(func(ctx *cli.Context, dest *sources.EngineAPIClient, _ log.Logger) error {
rpcClient, err := rpc.DialOptions(context.Background(), ctx.String("source")) rpcClient, err := rpc.DialOptions(context.Background(), ctx.String("source"))
if err != nil { if err != nil {
return fmt.Errorf("failed to dial engine source endpoint: %w", err) return fmt.Errorf("failed to dial engine source endpoint: %w", err)
...@@ -498,8 +583,7 @@ var ( ...@@ -498,8 +583,7 @@ var (
EngineSetForkchoiceCmd = &cli.Command{ EngineSetForkchoiceCmd = &cli.Command{
Name: "set-forkchoice", Name: "set-forkchoice",
Description: "Set forkchoice, specify unsafe, safe and finalized blocks by number", Description: "Set forkchoice, specify unsafe, safe and finalized blocks by number",
Flags: []cli.Flag{ Flags: withEngineFlags(
EngineEndpoint, EngineJWTPath,
&cli.Uint64Flag{ &cli.Uint64Flag{
Name: "unsafe", Name: "unsafe",
Usage: "Block number of block to set as latest block", Usage: "Block number of block to set as latest block",
...@@ -518,8 +602,8 @@ var ( ...@@ -518,8 +602,8 @@ var (
Required: true, Required: true,
EnvVars: prefixEnvVars("FINALIZED"), EnvVars: prefixEnvVars("FINALIZED"),
}, },
}, ),
Action: EngineAction(func(ctx *cli.Context, client client.RPC) error { Action: EngineAction(func(ctx *cli.Context, client *sources.EngineAPIClient, _ log.Logger) error {
return engine.SetForkchoice(ctx.Context, client, ctx.Uint64("finalized"), ctx.Uint64("safe"), ctx.Uint64("unsafe")) return engine.SetForkchoice(ctx.Context, client, ctx.Uint64("finalized"), ctx.Uint64("safe"), ctx.Uint64("unsafe"))
}), }),
} }
...@@ -527,8 +611,7 @@ var ( ...@@ -527,8 +611,7 @@ var (
EngineSetForkchoiceHashCmd = &cli.Command{ EngineSetForkchoiceHashCmd = &cli.Command{
Name: "set-forkchoice-by-hash", Name: "set-forkchoice-by-hash",
Description: "Set forkchoice, specify unsafe, safe and finalized blocks by hash", Description: "Set forkchoice, specify unsafe, safe and finalized blocks by hash",
Flags: []cli.Flag{ Flags: withEngineFlags(
EngineEndpoint, EngineJWTPath,
&cli.StringFlag{ &cli.StringFlag{
Name: "unsafe", Name: "unsafe",
Usage: "Block hash of block to set as latest block", Usage: "Block hash of block to set as latest block",
...@@ -547,8 +630,8 @@ var ( ...@@ -547,8 +630,8 @@ var (
Required: true, Required: true,
EnvVars: prefixEnvVars("FINALIZED"), EnvVars: prefixEnvVars("FINALIZED"),
}, },
}, ),
Action: EngineAction(func(ctx *cli.Context, client client.RPC) error { Action: EngineAction(func(ctx *cli.Context, client *sources.EngineAPIClient, _ log.Logger) error {
finalized := common.HexToHash(ctx.String("finalized")) finalized := common.HexToHash(ctx.String("finalized"))
safe := common.HexToHash(ctx.String("safe")) safe := common.HexToHash(ctx.String("safe"))
unsafe := common.HexToHash(ctx.String("unsafe")) unsafe := common.HexToHash(ctx.String("unsafe"))
...@@ -556,20 +639,44 @@ var ( ...@@ -556,20 +639,44 @@ var (
}), }),
} }
EngineRewindCmd = &cli.Command{
Name: "rewind",
Description: "Rewind chain by number (destructive!)",
Flags: withEngineFlags(
&cli.Uint64Flag{
Name: "to",
Usage: "Block number to rewind chain to",
Required: true,
EnvVars: prefixEnvVars("REWIND_TO"),
},
&cli.BoolFlag{
Name: "set-head",
Usage: "Whether to also call debug_setHead when rewinding",
EnvVars: prefixEnvVars("REWIND_SET_HEAD"),
},
),
Action: EngineAction(func(ctx *cli.Context, client *sources.EngineAPIClient, lgr log.Logger) error {
open, err := initOpenEngineRPC(ctx, lgr)
if err != nil {
return fmt.Errorf("failed to dial open RPC endpoint: %w", err)
}
return engine.Rewind(ctx.Context, lgr, client, open, ctx.Uint64("to"), ctx.Bool("set-head"))
}),
}
EngineJSONCmd = &cli.Command{ EngineJSONCmd = &cli.Command{
Name: "json", Name: "json",
Description: "read json values from remaining args, or STDIN, and use them as RPC params to call the engine RPC method (first arg)", Description: "read json values from remaining args, or STDIN, and use them as RPC params to call the engine RPC method (first arg)",
Flags: []cli.Flag{ Flags: withEngineFlags(
EngineEndpoint, EngineJWTPath,
&cli.BoolFlag{ &cli.BoolFlag{
Name: "stdin", Name: "stdin",
Usage: "Read params from stdin instead", Usage: "Read params from stdin instead",
Required: false, Required: false,
EnvVars: prefixEnvVars("STDIN"), EnvVars: prefixEnvVars("STDIN"),
}, },
}, ),
ArgsUsage: "<rpc-method-name> [params...]", ArgsUsage: "<rpc-method-name> [params...]",
Action: EngineAction(func(ctx *cli.Context, client client.RPC) error { Action: EngineAction(func(ctx *cli.Context, client *sources.EngineAPIClient, _ log.Logger) error {
if ctx.NArg() == 0 { if ctx.NArg() == 0 {
return fmt.Errorf("expected at least 1 argument: RPC method name") return fmt.Errorf("expected at least 1 argument: RPC method name")
} }
...@@ -580,7 +687,7 @@ var ( ...@@ -580,7 +687,7 @@ var (
} else { } else {
args = ctx.Args().Tail() args = ctx.Args().Tail()
} }
return engine.RawJSONInteraction(ctx.Context, client, ctx.Args().Get(0), args, r, ctx.App.Writer) return engine.RawJSONInteraction(ctx.Context, client.RPC, ctx.Args().Get(0), args, r, ctx.App.Writer)
}), }),
} }
) )
...@@ -603,7 +710,7 @@ var CheatCmd = &cli.Command{ ...@@ -603,7 +710,7 @@ var CheatCmd = &cli.Command{
var EngineCmd = &cli.Command{ var EngineCmd = &cli.Command{
Name: "engine", Name: "engine",
Usage: "Engine API commands to build/reorg/finalize blocks.", Usage: "Engine API commands to build/reorg/rewind/finalize/copy blocks.",
Description: "Each sub-command dials the engine API endpoint (with provided JWT secret) and then runs the action", Description: "Each sub-command dials the engine API endpoint (with provided JWT secret) and then runs the action",
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
EngineBlockCmd, EngineBlockCmd,
...@@ -613,6 +720,7 @@ var EngineCmd = &cli.Command{ ...@@ -613,6 +720,7 @@ var EngineCmd = &cli.Command{
EngineCopyPayloadCmd, EngineCopyPayloadCmd,
EngineSetForkchoiceCmd, EngineSetForkchoiceCmd,
EngineSetForkchoiceHashCmd, EngineSetForkchoiceHashCmd,
EngineRewindCmd,
EngineJSONCmd, EngineJSONCmd,
}, },
} }
...@@ -10,48 +10,27 @@ import ( ...@@ -10,48 +10,27 @@ import (
"strings" "strings"
"time" "time"
"github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc" "github.com/holiman/uint256"
"github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum-optimism/optimism/op-service/client"
"github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/sources"
) )
type PayloadAttributesV2 struct { const (
Timestamp uint64 `json:"timestamp"` methodEthGetBlockByNumber = "eth_getBlockByNumber"
Random common.Hash `json:"prevRandao"` methodDebugChainConfig = "debug_chainConfig"
SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient"` methodDebugSetHead = "debug_setHead"
Withdrawals []*types.Withdrawal `json:"withdrawals"` )
}
func (p PayloadAttributesV2) MarshalJSON() ([]byte, error) {
type PayloadAttributes struct {
Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"`
Random common.Hash `json:"prevRandao" gencodec:"required"`
SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient" gencodec:"required"`
Withdrawals []*types.Withdrawal `json:"withdrawals"`
}
var enc PayloadAttributes
enc.Timestamp = hexutil.Uint64(p.Timestamp)
enc.Random = p.Random
enc.SuggestedFeeRecipient = p.SuggestedFeeRecipient
enc.Withdrawals = make([]*types.Withdrawal, 0)
return json.Marshal(&enc)
}
func DialClient(ctx context.Context, endpoint string, jwtSecret [32]byte) (client.RPC, error) {
auth := node.NewJWTAuth(jwtSecret)
rpcClient, err := rpc.DialOptions(ctx, endpoint, rpc.WithHTTPAuth(auth)) func GetChainConfig(ctx context.Context, open client.RPC) (cfg *params.ChainConfig, err error) {
if err != nil { err = open.CallContext(ctx, &cfg, methodDebugChainConfig)
return nil, fmt.Errorf("failed to dial engine endpoint: %w", err) return
}
return client.NewBaseRPCClient(rpcClient), nil
} }
type RPCBlock struct { type RPCBlock struct {
...@@ -77,49 +56,67 @@ func getHeader(ctx context.Context, client client.RPC, method string, tag string ...@@ -77,49 +56,67 @@ func getHeader(ctx context.Context, client client.RPC, method string, tag string
return header, nil return header, nil
} }
func headSafeFinalized(ctx context.Context, client client.RPC) (head *types.Block, safe, finalized *types.Header, err error) { func headSafeFinalized(ctx context.Context, client client.RPC) (head, safe, finalized *types.Header, err error) {
head, err = getBlock(ctx, client, "eth_getBlockByNumber", "latest") head, err = getHeader(ctx, client, methodEthGetBlockByNumber, "latest")
if err != nil { if err != nil {
return nil, nil, nil, fmt.Errorf("failed to get block: %w", err) return nil, nil, nil, fmt.Errorf("failed to get latest: %w", err)
} }
safe, err = getHeader(ctx, client, "eth_getBlockByNumber", "safe") safe, fin, err := safeFinalized(ctx, client)
return head, safe, fin, err
}
func headBlockSafeFinalized(ctx context.Context, client client.RPC) (head *types.Block, safe, finalized *types.Header, err error) {
head, err = getBlock(ctx, client, methodEthGetBlockByNumber, "latest")
if err != nil { if err != nil {
return head, nil, nil, fmt.Errorf("failed to get safe block: %w", err) return nil, nil, nil, fmt.Errorf("failed to get latest: %w", err)
} }
finalized, err = getHeader(ctx, client, "eth_getBlockByNumber", "finalized") safe, fin, err := safeFinalized(ctx, client)
return head, safe, fin, err
}
func safeFinalized(ctx context.Context, client client.RPC) (safe, finalized *types.Header, err error) {
safe, err = getHeader(ctx, client, methodEthGetBlockByNumber, "safe")
if err != nil { if err != nil {
return head, safe, nil, fmt.Errorf("failed to get finalized block: %w", err) return nil, nil, fmt.Errorf("failed to get safe: %w", err)
} }
return head, safe, finalized, nil finalized, err = getHeader(ctx, client, methodEthGetBlockByNumber, "finalized")
if err != nil {
return safe, nil, fmt.Errorf("failed to get finalized: %w", err)
}
return safe, finalized, nil
} }
func insertBlock(ctx context.Context, client client.RPC, payload *engine.ExecutableData) error { func insertBlock(ctx context.Context, client *sources.EngineAPIClient, payloadEnv *eth.ExecutionPayloadEnvelope) error {
var payloadResult *engine.PayloadStatusV1 payload := payloadEnv.ExecutionPayload
if err := client.CallContext(ctx, &payloadResult, "engine_newPayloadV2", payload); err != nil { payloadResult, err := client.NewPayload(ctx, payload, payloadEnv.ParentBeaconBlockRoot)
return fmt.Errorf("failed to insert block %d: %w", payload.Number, err) if err != nil {
return fmt.Errorf("failed to insert block %d: %w", payload.BlockNumber, err)
} }
if payloadResult.Status != string(eth.ExecutionValid) { if payloadResult.Status != eth.ExecutionValid {
return fmt.Errorf("block insertion was not valid: %v", payloadResult.ValidationError) return fmt.Errorf("block insertion was not valid: %v", payloadResult.ValidationError)
} }
return nil return nil
} }
func updateForkchoice(ctx context.Context, client client.RPC, head, safe, finalized common.Hash) error { func updateForkchoice(ctx context.Context, client *sources.EngineAPIClient, head, safe, finalized common.Hash) error {
var post engine.ForkChoiceResponse res, err := client.ForkchoiceUpdate(ctx, &eth.ForkchoiceState{
if err := client.CallContext(ctx, &post, "engine_forkchoiceUpdatedV2",
engine.ForkchoiceStateV1{
HeadBlockHash: head, HeadBlockHash: head,
SafeBlockHash: safe, SafeBlockHash: safe,
FinalizedBlockHash: finalized, FinalizedBlockHash: finalized,
}, nil); err != nil { }, nil)
return fmt.Errorf("failed to set forkchoice with new block %s: %w", head, err) if err != nil {
return fmt.Errorf("failed to update forkchoice with new head %s: %w", head, err)
} }
if post.PayloadStatus.Status != string(eth.ExecutionValid) { if res.PayloadStatus.Status != eth.ExecutionValid {
return fmt.Errorf("post-block forkchoice update was not valid: %v", post.PayloadStatus.ValidationError) return fmt.Errorf("forkchoice update was not valid: %v", res.PayloadStatus.ValidationError)
} }
return nil return nil
} }
func debugSetHead(ctx context.Context, open client.RPC, head uint64) error {
return open.CallContext(ctx, nil, methodDebugSetHead, hexutil.Uint64(head))
}
type BlockBuildingSettings struct { type BlockBuildingSettings struct {
BlockTime uint64 BlockTime uint64
// skip a block; timestamps will still increase in multiples of BlockTime like L1, but there may be gaps. // skip a block; timestamps will still increase in multiples of BlockTime like L1, but there may be gaps.
...@@ -129,7 +126,7 @@ type BlockBuildingSettings struct { ...@@ -129,7 +126,7 @@ type BlockBuildingSettings struct {
BuildTime time.Duration BuildTime time.Duration
} }
func BuildBlock(ctx context.Context, client client.RPC, status *StatusData, settings *BlockBuildingSettings) (*engine.ExecutableData, error) { func BuildBlock(ctx context.Context, client *sources.EngineAPIClient, status *StatusData, settings *BlockBuildingSettings) (*eth.ExecutionPayloadEnvelope, error) {
timestamp := status.Head.Time + settings.BlockTime timestamp := status.Head.Time + settings.BlockTime
if settings.AllowGaps { if settings.AllowGaps {
now := uint64(time.Now().Unix()) now := uint64(time.Now().Unix())
...@@ -137,20 +134,17 @@ func BuildBlock(ctx context.Context, client client.RPC, status *StatusData, sett ...@@ -137,20 +134,17 @@ func BuildBlock(ctx context.Context, client client.RPC, status *StatusData, sett
timestamp = now - ((now - timestamp) % settings.BlockTime) timestamp = now - ((now - timestamp) % settings.BlockTime)
} }
} }
var pre engine.ForkChoiceResponse attrs := newPayloadAttributes(client.EngineVersionProvider(), timestamp, settings.Random, settings.FeeRecipient)
if err := client.CallContext(ctx, &pre, "engine_forkchoiceUpdatedV2", pre, err := client.ForkchoiceUpdate(ctx,
engine.ForkchoiceStateV1{ &eth.ForkchoiceState{
HeadBlockHash: status.Head.Hash, HeadBlockHash: status.Head.Hash,
SafeBlockHash: status.Safe.Hash, SafeBlockHash: status.Safe.Hash,
FinalizedBlockHash: status.Finalized.Hash, FinalizedBlockHash: status.Finalized.Hash,
}, PayloadAttributesV2{ }, attrs)
Timestamp: timestamp, if err != nil {
Random: settings.Random,
SuggestedFeeRecipient: settings.FeeRecipient,
}); err != nil {
return nil, fmt.Errorf("failed to set forkchoice when building new block: %w", err) return nil, fmt.Errorf("failed to set forkchoice when building new block: %w", err)
} }
if pre.PayloadStatus.Status != string(eth.ExecutionValid) { if pre.PayloadStatus.Status != eth.ExecutionValid {
return nil, fmt.Errorf("pre-block forkchoice update was not valid: %v", pre.PayloadStatus.ValidationError) return nil, fmt.Errorf("pre-block forkchoice update was not valid: %v", pre.PayloadStatus.ValidationError)
} }
...@@ -161,26 +155,45 @@ func BuildBlock(ctx context.Context, client client.RPC, status *StatusData, sett ...@@ -161,26 +155,45 @@ func BuildBlock(ctx context.Context, client client.RPC, status *StatusData, sett
case <-time.After(settings.BuildTime): case <-time.After(settings.BuildTime):
} }
var payload *engine.ExecutionPayloadEnvelope payload, err := client.GetPayload(ctx, eth.PayloadInfo{ID: *pre.PayloadID, Timestamp: timestamp})
if err := client.CallContext(ctx, &payload, "engine_getPayloadV2", pre.PayloadID); err != nil { if err != nil {
return nil, fmt.Errorf("failed to get payload %v, %d time after instructing engine to build it: %w", pre.PayloadID, settings.BuildTime, err) return nil, fmt.Errorf("failed to get payload %v, %d time after instructing engine to build it: %w", pre.PayloadID, settings.BuildTime, err)
} }
if err := insertBlock(ctx, client, payload.ExecutionPayload); err != nil { if err := insertBlock(ctx, client, payload); err != nil {
return nil, err return nil, err
} }
if err := updateForkchoice(ctx, client, payload.ExecutionPayload.BlockHash, status.Safe.Hash, status.Finalized.Hash); err != nil { if err := updateForkchoice(ctx, client, payload.ExecutionPayload.BlockHash, status.Safe.Hash, status.Finalized.Hash); err != nil {
return nil, err return nil, err
} }
return payload.ExecutionPayload, nil return payload, nil
}
func newPayloadAttributes(evp sources.EngineVersionProvider, timestamp uint64, prevRandao common.Hash, feeRecipient common.Address) *eth.PayloadAttributes {
pa := &eth.PayloadAttributes{
Timestamp: hexutil.Uint64(timestamp),
PrevRandao: eth.Bytes32(prevRandao),
SuggestedFeeRecipient: feeRecipient,
}
ver := evp.ForkchoiceUpdatedVersion(pa)
if ver == eth.FCUV2 || ver == eth.FCUV3 {
withdrawals := make(types.Withdrawals, 0)
pa.Withdrawals = &withdrawals
}
if ver == eth.FCUV3 {
pa.ParentBeaconBlockRoot = new(common.Hash)
}
return pa
} }
func Auto(ctx context.Context, metrics Metricer, client client.RPC, log log.Logger, shutdown <-chan struct{}, settings *BlockBuildingSettings) error { func Auto(ctx context.Context, metrics Metricer, client *sources.EngineAPIClient, log log.Logger, shutdown <-chan struct{}, settings *BlockBuildingSettings) error {
ticker := time.NewTicker(time.Millisecond * 100) ticker := time.NewTicker(time.Millisecond * 100)
defer ticker.Stop() defer ticker.Stop()
var lastPayload *engine.ExecutableData var lastPayload *eth.ExecutionPayload
var buildErr error var buildErr error
for { for {
select { select {
...@@ -194,7 +207,7 @@ func Auto(ctx context.Context, metrics Metricer, client client.RPC, log log.Logg ...@@ -194,7 +207,7 @@ func Auto(ctx context.Context, metrics Metricer, client client.RPC, log log.Logg
blockTime := time.Duration(settings.BlockTime) * time.Second blockTime := time.Duration(settings.BlockTime) * time.Second
lastTime := uint64(0) lastTime := uint64(0)
if lastPayload != nil { if lastPayload != nil {
lastTime = lastPayload.Timestamp lastTime = uint64(lastPayload.Timestamp)
} }
buildTriggerTime := time.Unix(int64(lastTime), 0).Add(blockTime - settings.BuildTime) buildTriggerTime := time.Unix(int64(lastTime), 0).Add(blockTime - settings.BuildTime)
...@@ -206,7 +219,7 @@ func Auto(ctx context.Context, metrics Metricer, client client.RPC, log log.Logg ...@@ -206,7 +219,7 @@ func Auto(ctx context.Context, metrics Metricer, client client.RPC, log log.Logg
buildTime = 10 * time.Millisecond buildTime = 10 * time.Millisecond
} }
buildErr = nil buildErr = nil
status, err := Status(ctx, client) status, err := Status(ctx, client.RPC)
if err != nil { if err != nil {
log.Error("failed to get pre-block engine status", "err", err) log.Error("failed to get pre-block engine status", "err", err)
metrics.RecordBlockFail() metrics.RecordBlockFail()
...@@ -220,7 +233,7 @@ func Auto(ctx context.Context, metrics Metricer, client client.RPC, log log.Logg ...@@ -220,7 +233,7 @@ func Auto(ctx context.Context, metrics Metricer, client client.RPC, log log.Logg
// There are no gap slots, so we just go back 32 blocks. // There are no gap slots, so we just go back 32 blocks.
if status.Head.Number%32 == 0 { if status.Head.Number%32 == 0 {
if status.Safe.Number+32 <= status.Head.Number { if status.Safe.Number+32 <= status.Head.Number {
safe, err := getHeader(ctx, client, "eth_getBlockByNumber", hexutil.Uint64(status.Head.Number-32).String()) safe, err := getHeader(ctx, client.RPC, methodEthGetBlockByNumber, hexutil.Uint64(status.Head.Number-32).String())
if err != nil { if err != nil {
buildErr = err buildErr = err
log.Error("failed to find block for new safe block progress", "err", err) log.Error("failed to find block for new safe block progress", "err", err)
...@@ -229,7 +242,7 @@ func Auto(ctx context.Context, metrics Metricer, client client.RPC, log log.Logg ...@@ -229,7 +242,7 @@ func Auto(ctx context.Context, metrics Metricer, client client.RPC, log log.Logg
status.Safe = eth.L1BlockRef{Hash: safe.Hash(), Number: safe.Number.Uint64(), Time: safe.Time, ParentHash: safe.ParentHash} status.Safe = eth.L1BlockRef{Hash: safe.Hash(), Number: safe.Number.Uint64(), Time: safe.Time, ParentHash: safe.ParentHash}
} }
if status.Finalized.Number+32 <= status.Safe.Number { if status.Finalized.Number+32 <= status.Safe.Number {
finalized, err := getHeader(ctx, client, "eth_getBlockByNumber", hexutil.Uint64(status.Safe.Number-32).String()) finalized, err := getHeader(ctx, client.RPC, methodEthGetBlockByNumber, hexutil.Uint64(status.Safe.Number-32).String())
if err != nil { if err != nil {
buildErr = err buildErr = err
log.Error("failed to find block for new finalized block progress", "err", err) log.Error("failed to find block for new finalized block progress", "err", err)
...@@ -239,7 +252,7 @@ func Auto(ctx context.Context, metrics Metricer, client client.RPC, log log.Logg ...@@ -239,7 +252,7 @@ func Auto(ctx context.Context, metrics Metricer, client client.RPC, log log.Logg
} }
} }
payload, err := BuildBlock(ctx, client, status, &BlockBuildingSettings{ payloadEnv, err := BuildBlock(ctx, client, status, &BlockBuildingSettings{
BlockTime: settings.BlockTime, BlockTime: settings.BlockTime,
AllowGaps: settings.AllowGaps, AllowGaps: settings.AllowGaps,
Random: settings.Random, Random: settings.Random,
...@@ -251,12 +264,16 @@ func Auto(ctx context.Context, metrics Metricer, client client.RPC, log log.Logg ...@@ -251,12 +264,16 @@ func Auto(ctx context.Context, metrics Metricer, client client.RPC, log log.Logg
log.Error("failed to produce block", "err", err) log.Error("failed to produce block", "err", err)
metrics.RecordBlockFail() metrics.RecordBlockFail()
} else { } else {
payload := payloadEnv.ExecutionPayload
lastPayload = payload lastPayload = payload
log.Info("created block", "hash", payload.BlockHash, "number", payload.Number, log.Info("created block", "hash", payload.BlockHash, "number", payload.BlockNumber,
"timestamp", payload.Timestamp, "txs", len(payload.Transactions), "timestamp", payload.Timestamp, "txs", len(payload.Transactions),
"gas", payload.GasUsed, "basefee", payload.BaseFeePerGas) "gas", payload.GasUsed, "basefee", payload.BaseFeePerGas)
basefee, _ := new(big.Float).SetInt(payload.BaseFeePerGas).Float64() basefee := (*uint256.Int)(&payload.BaseFeePerGas).Float64()
metrics.RecordBlockStats(payload.BlockHash, payload.Number, payload.Timestamp, uint64(len(payload.Transactions)), payload.GasUsed, basefee) metrics.RecordBlockStats(
payload.BlockHash, uint64(payload.BlockNumber), uint64(payload.Timestamp),
uint64(len(payload.Transactions)),
uint64(payload.GasUsed), basefee)
} }
} }
} }
...@@ -274,7 +291,7 @@ type StatusData struct { ...@@ -274,7 +291,7 @@ type StatusData struct {
} }
func Status(ctx context.Context, client client.RPC) (*StatusData, error) { func Status(ctx context.Context, client client.RPC) (*StatusData, error) {
head, safe, finalized, err := headSafeFinalized(ctx, client) head, safe, finalized, err := headBlockSafeFinalized(ctx, client)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -291,58 +308,73 @@ func Status(ctx context.Context, client client.RPC) (*StatusData, error) { ...@@ -291,58 +308,73 @@ func Status(ctx context.Context, client client.RPC) (*StatusData, error) {
// Copy takes the forkchoice state of copyFrom, and applies it to copyTo, and inserts the head-block. // Copy takes the forkchoice state of copyFrom, and applies it to copyTo, and inserts the head-block.
// The destination engine should then start syncing to this new chain if it has peers to do so. // The destination engine should then start syncing to this new chain if it has peers to do so.
func Copy(ctx context.Context, copyFrom client.RPC, copyTo client.RPC) error { func Copy(ctx context.Context, copyFrom client.RPC, copyTo *sources.EngineAPIClient) error {
copyHead, copySafe, copyFinalized, err := headSafeFinalized(ctx, copyFrom) copyHead, copySafe, copyFinalized, err := headBlockSafeFinalized(ctx, copyFrom)
if err != nil { if err != nil {
return err return err
} }
payloadEnv := engine.BlockToExecutableData(copyHead, nil, nil) payloadEnv, err := blockAsPayloadEnv(copyHead, copyTo.EngineVersionProvider())
if err != nil {
return err
}
if err := updateForkchoice(ctx, copyTo, copyHead.ParentHash(), copySafe.Hash(), copyFinalized.Hash()); err != nil { if err := updateForkchoice(ctx, copyTo, copyHead.ParentHash(), copySafe.Hash(), copyFinalized.Hash()); err != nil {
return err return err
} }
payload := payloadEnv.ExecutionPayload if err := insertBlock(ctx, copyTo, payloadEnv); err != nil {
if err := insertBlock(ctx, copyTo, payload); err != nil {
return err return err
} }
if err := updateForkchoice(ctx, copyTo, payload.BlockHash, copySafe.Hash(), copyFinalized.Hash()); err != nil { if err := updateForkchoice(ctx, copyTo,
payloadEnv.ExecutionPayload.BlockHash, copySafe.Hash(), copyFinalized.Hash()); err != nil {
return err return err
} }
return nil return nil
} }
// CopyPaylod takes the execution payload at number & applies it via NewPayload to copyTo // CopyPaylod takes the execution payload at number & applies it via NewPayload to copyTo
func CopyPayload(ctx context.Context, number uint64, copyFrom client.RPC, copyTo client.RPC) error { func CopyPayload(ctx context.Context, number uint64, copyFrom client.RPC, copyTo *sources.EngineAPIClient) error {
copyHead, err := getBlock(ctx, copyFrom, "eth_getBlockByNumber", hexutil.EncodeUint64(number)) copyHead, err := getBlock(ctx, copyFrom, methodEthGetBlockByNumber, hexutil.EncodeUint64(number))
if err != nil { if err != nil {
return err return err
} }
payloadEnv := engine.BlockToExecutableData(copyHead, nil, nil) payloadEnv, err := blockAsPayloadEnv(copyHead, copyTo.EngineVersionProvider())
payload := payloadEnv.ExecutionPayload if err != nil {
if err := insertBlock(ctx, copyTo, payload); err != nil { return err
}
if err := insertBlock(ctx, copyTo, payloadEnv); err != nil {
return err return err
} }
return nil return nil
} }
func SetForkchoice(ctx context.Context, client client.RPC, finalizedNum, safeNum, unsafeNum uint64) error { func blockAsPayloadEnv(block *types.Block, evp sources.EngineVersionProvider) (*eth.ExecutionPayloadEnvelope, error) {
var canyon *uint64
// hack: if we're calling at least FCUV2, get empty withdrawals by setting Canyon before the block time
if v := evp.ForkchoiceUpdatedVersion(&eth.PayloadAttributes{Timestamp: hexutil.Uint64(block.Time())}); v != eth.FCUV1 {
canyon = new(uint64)
}
return eth.BlockAsPayloadEnv(block, canyon)
}
func SetForkchoice(ctx context.Context, client *sources.EngineAPIClient, finalizedNum, safeNum, unsafeNum uint64) error {
if unsafeNum < safeNum { if unsafeNum < safeNum {
return fmt.Errorf("cannot set unsafe (%d) < safe (%d)", unsafeNum, safeNum) return fmt.Errorf("cannot set unsafe (%d) < safe (%d)", unsafeNum, safeNum)
} }
if safeNum < finalizedNum { if safeNum < finalizedNum {
return fmt.Errorf("cannot set safe (%d) < finalized (%d)", safeNum, finalizedNum) return fmt.Errorf("cannot set safe (%d) < finalized (%d)", safeNum, finalizedNum)
} }
head, err := getHeader(ctx, client, "eth_getBlockByNumber", "latest") head, err := getHeader(ctx, client.RPC, methodEthGetBlockByNumber, "latest")
if err != nil { if err != nil {
return fmt.Errorf("failed to get latest block: %w", err) return fmt.Errorf("failed to get latest block: %w", err)
} }
if unsafeNum > head.Number.Uint64() { if unsafeNum > head.Number.Uint64() {
return fmt.Errorf("cannot set unsafe (%d) > latest (%d)", unsafeNum, head.Number.Uint64()) return fmt.Errorf("cannot set unsafe (%d) > latest (%d)", unsafeNum, head.Number.Uint64())
} }
finalizedHeader, err := getHeader(ctx, client, "eth_getBlockByNumber", hexutil.Uint64(finalizedNum).String()) finalizedHeader, err := getHeader(ctx, client.RPC, methodEthGetBlockByNumber, hexutil.Uint64(finalizedNum).String())
if err != nil { if err != nil {
return fmt.Errorf("failed to get block %d to mark finalized: %w", finalizedNum, err) return fmt.Errorf("failed to get block %d to mark finalized: %w", finalizedNum, err)
} }
safeHeader, err := getHeader(ctx, client, "eth_getBlockByNumber", hexutil.Uint64(safeNum).String()) safeHeader, err := getHeader(ctx, client.RPC, methodEthGetBlockByNumber, hexutil.Uint64(safeNum).String())
if err != nil { if err != nil {
return fmt.Errorf("failed to get block %d to mark safe: %w", safeNum, err) return fmt.Errorf("failed to get block %d to mark safe: %w", safeNum, err)
} }
...@@ -352,13 +384,50 @@ func SetForkchoice(ctx context.Context, client client.RPC, finalizedNum, safeNum ...@@ -352,13 +384,50 @@ func SetForkchoice(ctx context.Context, client client.RPC, finalizedNum, safeNum
return nil return nil
} }
func SetForkchoiceByHash(ctx context.Context, client client.RPC, finalized, safe, unsafe common.Hash) error { func SetForkchoiceByHash(ctx context.Context, client *sources.EngineAPIClient, finalized, safe, unsafe common.Hash) error {
if err := updateForkchoice(ctx, client, unsafe, safe, finalized); err != nil { if err := updateForkchoice(ctx, client, unsafe, safe, finalized); err != nil {
return fmt.Errorf("failed to update forkchoice: %w", err) return fmt.Errorf("failed to update forkchoice: %w", err)
} }
return nil return nil
} }
func Rewind(ctx context.Context, lgr log.Logger, client *sources.EngineAPIClient, open client.RPC, to uint64, setHead bool) error {
unsafe, err := getHeader(ctx, open, methodEthGetBlockByNumber, hexutil.Uint64(to).String())
if err != nil {
return fmt.Errorf("failed to get header %d: %w", to, err)
}
toUnsafe := eth.HeaderBlockID(unsafe)
latest, safe, finalized, err := headSafeFinalized(ctx, open)
if err != nil {
return fmt.Errorf("failed to get current heads: %w", err)
}
// when rewinding, don't increase unsafe/finalized tags
toSafe, toFinalized := toUnsafe, toUnsafe
if safe.Number.Uint64() < to {
toSafe = eth.HeaderBlockID(safe)
}
if finalized.Number.Uint64() < to {
toFinalized = eth.HeaderBlockID(finalized)
}
lgr.Info("Rewinding chain",
"setHead", setHead,
"latest", eth.HeaderBlockID(latest),
"unsafe", toUnsafe,
"safe", toSafe,
"finalized", toFinalized,
)
if setHead {
lgr.Debug("Calling "+methodDebugSetHead, "head", to)
if err := debugSetHead(ctx, open, to); err != nil {
return fmt.Errorf("failed to setHead %d: %w", to, err)
}
}
return SetForkchoiceByHash(ctx, client, toFinalized.Hash, toSafe.Hash, toUnsafe.Hash)
}
func RawJSONInteraction(ctx context.Context, client client.RPC, method string, args []string, input io.Reader, output io.Writer) error { func RawJSONInteraction(ctx context.Context, client client.RPC, method string, args []string, input io.Reader, output io.Writer) error {
var params []any var params []any
if input != nil { if input != nil {
......
package engine
import (
"strconv"
"github.com/ethereum-optimism/optimism/op-service/eth"
)
type StaticVersionProvider int
func (v StaticVersionProvider) ForkchoiceUpdatedVersion(*eth.PayloadAttributes) eth.EngineAPIMethod {
switch int(v) {
case 1:
return eth.FCUV1
case 2:
return eth.FCUV2
case 3:
return eth.FCUV3
default:
panic("invalid Engine API version: " + strconv.Itoa(int(v)))
}
}
func (v StaticVersionProvider) NewPayloadVersion(uint64) eth.EngineAPIMethod {
switch int(v) {
case 1, 2:
return eth.NewPayloadV2
case 3:
return eth.NewPayloadV3
default:
panic("invalid Engine API version: " + strconv.Itoa(int(v)))
}
}
func (v StaticVersionProvider) GetPayloadVersion(uint64) eth.EngineAPIMethod {
switch int(v) {
case 1, 2:
return eth.GetPayloadV2
case 3:
return eth.GetPayloadV3
default:
panic("invalid Engine API version: " + strconv.Itoa(int(v)))
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment