wheel

package
v1.16.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 19, 2026 License: MIT Imports: 28 Imported by: 0

README

op-wheel

Issues: monorepo

Pull requests: monorepo

op-wheel is a CLI tool to direct the engine one way or the other with DB cheats and Engine API routines.

It was named the "wheel" because of two reasons:

  • Figuratively, it allows to steer the stack, an interface for a driver (like the op-node sub-component) to control the execution engine (e.g. op-geth).
  • Idiomatically, like the Unix wheel-bit and its slang origins: empower a user to execute restricted commands, or more generally just someone with great power or influence.

Quickstart

Cheat utils

Cheating commands to modify a Geth database without corresponding in-protocol change.

The cheat sub-command has sub-commands for interacting with the DB, making patches, and dumping debug data.

Note that the validity of state-changes, as applied through patches, does not get checked until the block is re-processed. This can be used to trick the node into things like hypothetical test-states or shadow-forks without diverging the block-hashes.

To run:

go run ./op-wheel/cmd cheat --help
Engine utils

Engine API commands to build/reorg/rewind/finalize/copy blocks.

Each sub-command dials the engine API endpoint (with provided JWT secret) and then runs the action.

To run:

go run ./op-wheel/cmd engine --help

Usage

Build from source
# from op-wheel dir:
just op-wheel
./bin/op-wheel --help
Run from source
# from op-wheel dir:
go run ./cmd --help
Build docker image

See op-wheel docker-bake target.

Product

op-wheel is a tool for expert-users to perform advanced data recoveries, tests and overrides. This tool optimizes for reusability of these expert actions, to make them less error-prone.

This is not part of a standard release / process, as this tool is not used commonly, and the end-user is expected to be familiar with building from source.

Actions that are common enough to be used at least once by the average end-user should be part of the op-node or other standard op-stack release.

Design principles

Design for an expert-user: this tool aims to provide full control over critical op-stack data such as the engine-API and database itself, without hiding important information.

However, even as expert-user, wrong assumptions can be made. Defaults should aim to reduce errors, and leave the stack in a safe state to recover from.

Failure modes

This tool is not used in the happy-path, but can be critical during expert-recovery of advanced failure modes. E.g. database recovery after Geth database corruption, or manual forkchoice overrides. Most importantly, each CLI command used for recovery aims to be verbose, and avoids leaving an inconsistent state after failed or interrupted recovery.

Testing

This is a test-utility more than a production tool, and thus does currently not have test-coverage of its own. However, when it is used as tool during (dev/test) chain or node issues, usage does inform fixes/improvements.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	GlobalGethLogLvlFlag = &cli.GenericFlag{
		Name:    "geth-log-level",
		Usage:   "Set the global geth logging level",
		EnvVars: prefixEnvVars("GETH_LOG_LEVEL"),
		Value:   oplog.NewLevelFlagValue(log.LevelError),
	}
	DataDirFlag = &cli.StringFlag{
		Name:      "data-dir",
		Usage:     "Geth data dir location.",
		Required:  true,
		TakesFile: true,
		EnvVars:   prefixEnvVars("DATA_DIR"),
	}
	EngineEndpoint = &cli.StringFlag{
		Name:     "engine",
		Usage:    "Authenticated Engine API RPC endpoint, can be HTTP/WS/IPC",
		Required: true,
		Value:    "http://localhost:8551/",
		EnvVars:  prefixEnvVars("ENGINE"),
	}
	EngineJWT = &cli.StringFlag{
		Name:    "engine.jwt-secret",
		Usage:   "JWT secret used to authenticate Engine API communication with. Takes precedence over engine.jwt-secret-path.",
		EnvVars: prefixEnvVars("ENGINE_JWT_SECRET"),
	}
	EngineJWTPath = &cli.StringFlag{
		Name:      "engine.jwt-secret-path",
		Usage:     "Path to JWT secret file used to authenticate Engine API communication with.",
		TakesFile: true,
		EnvVars:   prefixEnvVars("ENGINE_JWT_SECRET_PATH"),
	}
	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{
		Name:    "fee-recipient",
		Usage:   "fee-recipient of the block building",
		EnvVars: prefixEnvVars("FEE_RECIPIENT"),
		Value:   &TextFlag[*common.Address]{Value: &common.Address{1: 0x13, 2: 0x37}},
	}
	RandaoFlag = &cli.GenericFlag{
		Name:    "randao",
		Usage:   "randao value of the block building",
		EnvVars: prefixEnvVars("RANDAO"),
		Value:   &TextFlag[*common.Hash]{Value: &common.Hash{1: 0x13, 2: 0x37}},
	}
	BlockTimeFlag = &cli.Uint64Flag{
		Name:    "block-time",
		Usage:   "block time, interval of timestamps between blocks to build, in seconds",
		EnvVars: prefixEnvVars("BLOCK_TIME"),
		Value:   12,
	}
	BuildingTime = &cli.DurationFlag{
		Name:    "building-time",
		Usage:   "duration of of block building, this should be set to something lower than the block time.",
		EnvVars: prefixEnvVars("BUILDING_TIME"),
		Value:   time.Second * 6,
	}
	AllowGaps = &cli.BoolFlag{
		Name:    "allow-gaps",
		Usage:   "allow gaps in block building, like missed slots on the beacon chain.",
		EnvVars: prefixEnvVars("ALLOW_GAPS"),
	}
)
View Source
var (
	CheatStorageGetCmd = &cli.Command{
		Name:    "get",
		Aliases: []string{"read"},
		Flags: []cli.Flag{
			DataDirFlag,
			addrFlag("address", "Address to read storage of"),
			hashFlag("key", "key in storage of address to read value"),
		},
		Action: CheatAction(true, func(ctx *cli.Context, ch *cheat.Cheater) error {
			return ch.RunAndClose(cheat.StorageGet(addrFlagValue("address", ctx), hashFlagValue("key", ctx), ctx.App.Writer))
		}),
	}
	CheatStorageSetCmd = &cli.Command{
		Name:    "set",
		Aliases: []string{"write"},
		Flags: []cli.Flag{
			DataDirFlag,
			addrFlag("address", "Address to write storage of"),
			hashFlag("key", "key in storage of address to set value of"),
			hashFlag("value", "the value to write"),
		},
		Action: CheatAction(false, func(ctx *cli.Context, ch *cheat.Cheater) error {
			return ch.RunAndClose(cheat.StorageSet(addrFlagValue("address", ctx), hashFlagValue("key", ctx), hashFlagValue("value", ctx)))
		}),
	}
	CheatStorageReadAll = &cli.Command{
		Name:    "read-all",
		Aliases: []string{"get-all"},
		Usage:   "Read all storage of the given account",
		Flags:   []cli.Flag{DataDirFlag, addrFlag("address", "Address to read all storage of")},
		Action: CheatAction(true, func(ctx *cli.Context, ch *cheat.Cheater) error {
			return ch.RunAndClose(cheat.StorageReadAll(addrFlagValue("address", ctx), ctx.App.Writer))
		}),
	}
	CheatStorageDiffCmd = &cli.Command{
		Name:  "diff",
		Usage: "Diff the storage of accounts A and B",
		Flags: []cli.Flag{DataDirFlag, hashFlag("a", "address of account A"), hashFlag("b", "address of account B")},
		Action: CheatAction(true, func(ctx *cli.Context, ch *cheat.Cheater) error {
			return ch.RunAndClose(cheat.StorageDiff(ctx.App.Writer, addrFlagValue("a", ctx), addrFlagValue("b", ctx)))
		}),
	}
	CheatStoragePatchCmd = &cli.Command{
		Name:  "patch",
		Usage: "Apply storage patch from STDIN to the given account address",
		Flags: []cli.Flag{DataDirFlag, addrFlag("address", "Address to patch storage of")},
		Action: CheatAction(false, func(ctx *cli.Context, ch *cheat.Cheater) error {
			return ch.RunAndClose(cheat.StoragePatch(os.Stdin, addrFlagValue("address", ctx)))
		}),
	}
	CheatStorageCmd = &cli.Command{
		Name: "storage",
		Subcommands: []*cli.Command{
			CheatStorageGetCmd,
			CheatStorageSetCmd,
			CheatStorageReadAll,
			CheatStorageDiffCmd,
			CheatStoragePatchCmd,
		},
	}
	CheatSetBalanceCmd = &cli.Command{
		Name: "balance",
		Flags: []cli.Flag{
			DataDirFlag,
			addrFlag("address", "Address to change balance of"),
			bigFlag("balance", "New balance of the account"),
		},
		Action: CheatAction(false, func(ctx *cli.Context, ch *cheat.Cheater) error {
			return ch.RunAndClose(cheat.SetBalance(addrFlagValue("address", ctx), bigFlagValue("balance", ctx)))
		}),
	}
	CheatSetCodeCmd = &cli.Command{
		Name: "code",
		Flags: []cli.Flag{
			DataDirFlag,
			addrFlag("address", "Address to change code of"),
			bytesFlag("code", "New code of the account"),
		},
		Action: CheatAction(false, func(ctx *cli.Context, ch *cheat.Cheater) error {
			return ch.RunAndClose(cheat.SetCode(addrFlagValue("address", ctx), bytesFlagValue("code", ctx)))
		}),
	}
	CheatSetNonceCmd = &cli.Command{
		Name: "nonce",
		Flags: []cli.Flag{
			DataDirFlag,
			addrFlag("address", "Address to change nonce of"),
			bigFlag("nonce", "New nonce of the account"),
		},
		Action: CheatAction(false, func(ctx *cli.Context, ch *cheat.Cheater) error {
			return ch.RunAndClose(cheat.SetNonce(addrFlagValue("address", ctx), bigs.Uint64Strict(bigFlagValue("balance", ctx))))
		}),
	}
	CheatPrintHeadBlock = &cli.Command{
		Name:  "head-block",
		Usage: "dump head block as JSON",
		Flags: []cli.Flag{
			DataDirFlag,
		},
		Action: CheatRawDBAction(true, func(c *cli.Context, db ethdb.Database) error {
			enc := json.NewEncoder(c.App.Writer)
			enc.SetIndent("  ", "  ")
			block := rawdb.ReadHeadBlock(db)
			if block == nil {
				return enc.Encode(nil)
			}
			return enc.Encode(engine.RPCBlock{
				Header:       *block.Header(),
				Transactions: block.Transactions(),
			})
		}),
	}
	CheatPrintHeadHeader = &cli.Command{
		Name:  "head-header",
		Usage: "dump head header as JSON",
		Flags: []cli.Flag{
			DataDirFlag,
		},
		Action: CheatRawDBAction(true, func(c *cli.Context, db ethdb.Database) error {
			enc := json.NewEncoder(c.App.Writer)
			enc.SetIndent("  ", "  ")
			return enc.Encode(rawdb.ReadHeadHeader(db))
		}),
	}
	EngineBlockCmd = &cli.Command{
		Name:  "block",
		Usage: "build the next block using the Engine API",
		Flags: withEngineFlags(
			FeeRecipientFlag, RandaoFlag, BlockTimeFlag, BuildingTime, AllowGaps,
		),

		Action: EngineAction(func(ctx *cli.Context, client *sources.EngineAPIClient, _ log.Logger) error {
			settings := ParseBuildingArgs(ctx)
			status, err := engine.Status(context.Background(), client.RPC)
			if err != nil {
				return err
			}
			payloadEnv, err := engine.BuildBlock(context.Background(), client, status, settings)
			if err != nil {
				return err
			}
			fmt.Fprintln(ctx.App.Writer, payloadEnv.ExecutionPayload.BlockHash)
			return nil
		}),
	}
	EngineAutoCmd = &cli.Command{
		Name:        "auto",
		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.",
		Flags: append(withEngineFlags(
			FeeRecipientFlag, RandaoFlag, BlockTimeFlag, BuildingTime, AllowGaps),
			opmetrics.CLIFlags(envVarPrefix)...),
		Action: EngineAction(func(ctx *cli.Context, client *sources.EngineAPIClient, l log.Logger) error {
			settings := ParseBuildingArgs(ctx)

			metricsCfg := opmetrics.ReadCLIConfig(ctx)

			return opservice.CloseAction(ctx.Context, func(ctx context.Context) error {
				registry := opmetrics.NewRegistry()
				metrics := engine.NewMetrics("wheel", registry)
				if metricsCfg.Enabled {
					l.Info("starting metrics server", "addr", metricsCfg.ListenAddr, "port", metricsCfg.ListenPort)
					metricsSrv, err := opmetrics.StartServer(registry, metricsCfg.ListenAddr, metricsCfg.ListenPort)
					if err != nil {
						return fmt.Errorf("failed to start metrics server: %w", err)
					}
					defer func() {
						if err := metricsSrv.Stop(context.Background()); err != nil {
							l.Error("failed to stop metrics server: %w", err)
						}
					}()
				}
				return engine.Auto(ctx, metrics, client, l, settings)
			})
		}),
	}
	EngineStatusCmd = &cli.Command{
		Name:  "status",
		Flags: withEngineFlags(),
		Action: EngineAction(func(ctx *cli.Context, client *sources.EngineAPIClient, _ log.Logger) error {
			stat, err := engine.Status(context.Background(), client.RPC)
			if err != nil {
				return err
			}
			enc := json.NewEncoder(ctx.App.Writer)
			enc.SetIndent("", "  ")
			return enc.Encode(stat)
		}),
	}
	EngineCopyCmd = &cli.Command{
		Name: "copy",
		Flags: withEngineFlags(
			&cli.StringFlag{
				Name:     "source",
				Usage:    "Unauthenticated regular eth JSON RPC to pull block data from, can be HTTP/WS/IPC.",
				Required: true,
				EnvVars:  prefixEnvVars("SOURCE"),
			},
		),
		Action: EngineAction(func(ctx *cli.Context, dest *sources.EngineAPIClient, _ log.Logger) error {
			rpcClient, err := rpc.DialOptions(context.Background(), ctx.String("source"))
			if err != nil {
				return fmt.Errorf("failed to dial engine source endpoint: %w", err)
			}
			source := client.NewBaseRPCClient(rpcClient)
			return engine.Copy(context.Background(), source, dest)
		}),
	}

	EngineCopyPayloadCmd = &cli.Command{
		Name:        "copy-payload",
		Description: "Take the block by number from source and insert it to the engine with NewPayload. No other calls are made.",
		Flags: withEngineFlags(
			&cli.StringFlag{
				Name:     "source",
				Usage:    "Unauthenticated regular eth JSON RPC to pull block data from, can be HTTP/WS/IPC.",
				Required: true,
				EnvVars:  prefixEnvVars("SOURCE"),
			},
			&cli.Uint64Flag{
				Name:     "number",
				Usage:    "Block number to copy from the source",
				Required: true,
				EnvVars:  prefixEnvVars("NUMBER"),
			},
		),
		Action: EngineAction(func(ctx *cli.Context, dest *sources.EngineAPIClient, _ log.Logger) error {
			rpcClient, err := rpc.DialOptions(context.Background(), ctx.String("source"))
			if err != nil {
				return fmt.Errorf("failed to dial engine source endpoint: %w", err)
			}
			source := client.NewBaseRPCClient(rpcClient)
			return engine.CopyPayload(context.Background(), ctx.Uint64("number"), source, dest)
		}),
	}

	EngineSetForkchoiceCmd = &cli.Command{
		Name:        "set-forkchoice",
		Description: "Set forkchoice, specify unsafe, safe and finalized blocks by number",
		Flags: withEngineFlags(
			&cli.Uint64Flag{
				Name:     "unsafe",
				Usage:    "Block number of block to set as latest block",
				Required: true,
				EnvVars:  prefixEnvVars("UNSAFE"),
			},
			&cli.Uint64Flag{
				Name:     "safe",
				Usage:    "Block number of block to set as safe block",
				Required: true,
				EnvVars:  prefixEnvVars("SAFE"),
			},
			&cli.Uint64Flag{
				Name:     "finalized",
				Usage:    "Block number of block to set as finalized block",
				Required: true,
				EnvVars:  prefixEnvVars("FINALIZED"),
			},
		),
		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"))
		}),
	}

	EngineSetForkchoiceHashCmd = &cli.Command{
		Name:        "set-forkchoice-by-hash",
		Description: "Set forkchoice, specify unsafe, safe and finalized blocks by hash",
		Flags: withEngineFlags(
			&cli.StringFlag{
				Name:     "unsafe",
				Usage:    "Block hash of block to set as latest block",
				Required: true,
				EnvVars:  prefixEnvVars("UNSAFE"),
			},
			&cli.StringFlag{
				Name:     "safe",
				Usage:    "Block hash of block to set as safe block",
				Required: true,
				EnvVars:  prefixEnvVars("SAFE"),
			},
			&cli.StringFlag{
				Name:     "finalized",
				Usage:    "Block hash of block to set as finalized block",
				Required: true,
				EnvVars:  prefixEnvVars("FINALIZED"),
			},
		),
		Action: EngineAction(func(ctx *cli.Context, client *sources.EngineAPIClient, _ log.Logger) error {
			finalized := common.HexToHash(ctx.String("finalized"))
			safe := common.HexToHash(ctx.String("safe"))
			unsafe := common.HexToHash(ctx.String("unsafe"))
			return engine.SetForkchoiceByHash(ctx.Context, client, finalized, safe, unsafe)
		}),
	}

	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{
		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)",
		Flags: withEngineFlags(
			&cli.BoolFlag{
				Name:     "stdin",
				Usage:    "Read params from stdin instead",
				Required: false,
				EnvVars:  prefixEnvVars("STDIN"),
			},
		),
		ArgsUsage: "<rpc-method-name> [params...]",
		Action: EngineAction(func(ctx *cli.Context, client *sources.EngineAPIClient, _ log.Logger) error {
			if ctx.NArg() == 0 {
				return fmt.Errorf("expected at least 1 argument: RPC method name")
			}
			var r io.Reader
			var args []string
			if ctx.Bool("stdin") {
				r = ctx.App.Reader
			} else {
				args = ctx.Args().Tail()
			}
			return engine.RawJSONInteraction(ctx.Context, client.RPC, ctx.Args().Get(0), args, r, ctx.App.Writer)
		}),
	}
)
View Source
var CheatCmd = &cli.Command{
	Name:  "cheat",
	Usage: "Cheating commands to modify a Geth database.",
	Description: "Each sub-command opens a Geth database, applies the cheat, and then saves and closes the database." +
		"The Geth node will live in its own false reality, other nodes cannot sync the cheated state if they process the blocks.",
	Subcommands: []*cli.Command{
		CheatStorageCmd,
		CheatSetBalanceCmd,
		CheatSetCodeCmd,
		CheatSetNonceCmd,
		CheatPrintHeadBlock,
		CheatPrintHeadHeader,
	},
}
View Source
var EngineCmd = &cli.Command{
	Name:        "engine",
	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",
	Subcommands: []*cli.Command{
		EngineBlockCmd,
		EngineAutoCmd,
		EngineStatusCmd,
		EngineCopyCmd,
		EngineCopyPayloadCmd,
		EngineSetForkchoiceCmd,
		EngineSetForkchoiceHashCmd,
		EngineRewindCmd,
		EngineJSONCmd,
	},
}

Functions

func CheatAction

func CheatAction(readOnly bool, fn func(ctx *cli.Context, ch *cheat.Cheater) error) cli.ActionFunc

func CheatRawDBAction

func CheatRawDBAction(readOnly bool, fn func(ctx *cli.Context, db ethdb.Database) error) cli.ActionFunc

func EngineAction

func EngineAction(fn func(ctx *cli.Context, client *sources.EngineAPIClient, lgr log.Logger) error) cli.ActionFunc

func ParseBuildingArgs

func ParseBuildingArgs(ctx *cli.Context) *engine.BlockBuildingSettings

Types

type Text

type Text interface {
	encoding.TextUnmarshaler
	fmt.Stringer
	comparable
}

type TextFlag

type TextFlag[T Text] struct {
	Value T
}

func (*TextFlag[T]) Clone added in v1.2.0

func (a *TextFlag[T]) Clone() any

func (*TextFlag[T]) Get

func (a *TextFlag[T]) Get() T

func (*TextFlag[T]) Set

func (a *TextFlag[T]) Set(value string) error

func (*TextFlag[T]) String

func (a *TextFlag[T]) String() string

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL