-
Notifications
You must be signed in to change notification settings - Fork 3.8k
op-chain-ops: add check-jovian cmd #18269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+378
−0
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
3492c93
op-chain-ops: add check-jovian cmd
geoknee fd70f96
op-chain-ops: add extra data format validation to check-jovian command
geoknee e6834db
op-chain-ops: enhance block header validation in check-jovian command…
geoknee 8cca10c
refactor using op-geth library fns
geoknee 5744d9d
Rename checkBlockHeader to checkBlock and improve Jovian activation
geoknee 3a283eb
Add secret-key flag to send tx-to-self
geoknee 8d25fd0
lint
geoknee 0f3c769
Require non-zero BlobGasUsed for Jovian
geoknee 56e27d9
Use txmgr to send and wait for tx in check-jovian
geoknee 56b9160
Validate BlobGasUsed against DA footprint
geoknee d387dde
Use provided context for transaction send
geoknee 8e80276
Update op-chain-ops/cmd/check-jovian/main.go
geoknee 5b97b0c
Remove comment about BlobGasUsed
geoknee 51ab4bc
Document secret key option for check-jovian
geoknee ab394b9
Return error on zero DA scalar; harden blobGasUsed
geoknee a2ac927
Annotate txmgr logger with component field
geoknee d4a798f
Return error for blocks with no transactions
geoknee 4a94ed4
Merge remote-tracking branch 'origin/develop' into gk/check-jovian
geoknee File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| # check-jovian | ||
|
|
||
| A tool to verify that the Jovian upgrade has been successfully applied to an OP Stack chain. | ||
|
|
||
| ## Overview | ||
|
|
||
| This tool checks four key aspects of the Jovian upgrade: | ||
|
|
||
| 1. **GasPriceOracle Contract**: Verifies that `GasPriceOracle.isJovian()` returns `true` | ||
| 2. **L1Block Contract**: Verifies that `L1Block.DAFootprintGasScalar()` returns a valid number | ||
| 3. **Block Headers**: Verifies that the latest block header has a non-nil `BlobGasUsed` field (non-zero is hard evidence of Jovian, zero is inconclusive) | ||
| 4. **Extra Data Format**: Verifies that the block header `extraData` has the correct Jovian format (17 bytes with version=1, EIP-1559 params, and minimum base fee) | ||
|
|
||
| ## Usage | ||
|
|
||
| ### Prerequisites | ||
|
|
||
| Set the L2 RPC endpoint via environment variable: | ||
| ```bash | ||
| export CHECK_JOVIAN_L2=http://localhost:9545 | ||
| ``` | ||
|
|
||
| Or use the command-line flag: | ||
| ```bash | ||
| --l2 http://localhost:9545 | ||
| ``` | ||
|
|
||
| To execute the most thorough checks, you may pass a secret key via the `CHECK_JOVIAN_SECRET` environment variable: | ||
| ```bash | ||
| export CHECK_JOVIAN_SECRET=your-secret-key | ||
| ``` | ||
|
|
||
|
|
||
| Similarly, you can pass the secret key using the `--secret` flag: | ||
| ```bash | ||
| --secret your-secret-key | ||
| ``` | ||
|
|
||
| ### Commands | ||
|
|
||
| #### Check all Jovian features | ||
| ```bash | ||
| go run . all | ||
| ``` | ||
|
|
||
| #### Check individual features | ||
|
|
||
| Check GasPriceOracle contract: | ||
| ```bash | ||
| go run . contracts gpo | ||
| ``` | ||
|
|
||
| Check L1Block contract: | ||
| ```bash | ||
| go run . contracts l1block | ||
| ``` | ||
|
|
||
| Check block header: | ||
| ```bash | ||
| go run . block-header | ||
| ``` | ||
|
|
||
| Check extra data format: | ||
| ```bash | ||
| go run . extra-data | ||
| ``` | ||
|
|
||
| ## Build | ||
|
|
||
| From the `optimism` directory: | ||
| ```bash | ||
| go build ./op-chain-ops/cmd/check-jovian | ||
| ``` | ||
|
|
||
| ## Implementation Details | ||
|
|
||
| The tool uses the `op-e2e/bindings` package to interact with the L2 contracts and verify: | ||
|
|
||
| - **GasPriceOracle.isJovian**: Returns `true` after the Jovian upgrade is activated | ||
| - **L1Block.DAFootprintGasScalar**: Returns the DA footprint gas scalar value (warns if 0, as SystemConfig needs to update) | ||
| - **Block Header BlobGasUsed**: Non-nil after Jovian activation (non-zero value is hard evidence of Jovian, zero is inconclusive as it could indicate an empty block) | ||
| - **Extra Data Format**: Validates the header `extraData` field contains: | ||
| - 17 bytes total length | ||
| - Version byte = 1 (Jovian version) | ||
| - Denominator (uint32, bytes 1-5) | ||
| - Elasticity (uint32, bytes 5-9) | ||
| - Minimum Base Fee (uint64, bytes 9-17) | ||
|
|
||
| ## Pattern | ||
|
|
||
| This tool follows the same pattern as `check-ecotone` and `check-fjord`, providing a systematic way to verify upgrade completion. |
sebastianst marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,287 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "math/big" | ||
| "os" | ||
| "strings" | ||
|
|
||
| "github.com/urfave/cli/v2" | ||
|
|
||
| "github.com/ethereum/go-ethereum/consensus/misc/eip1559" | ||
| "github.com/ethereum/go-ethereum/core/types" | ||
| "github.com/ethereum/go-ethereum/ethclient" | ||
| "github.com/ethereum/go-ethereum/log" | ||
|
|
||
| "github.com/ethereum-optimism/optimism/op-core/predeploys" | ||
| "github.com/ethereum-optimism/optimism/op-e2e/bindings" | ||
| op_service "github.com/ethereum-optimism/optimism/op-service" | ||
| "github.com/ethereum-optimism/optimism/op-service/cliapp" | ||
| "github.com/ethereum-optimism/optimism/op-service/ctxinterrupt" | ||
| oplog "github.com/ethereum-optimism/optimism/op-service/log" | ||
| "github.com/ethereum-optimism/optimism/op-service/txmgr" | ||
| "github.com/ethereum-optimism/optimism/op-service/txmgr/metrics" | ||
| ) | ||
|
|
||
| func main() { | ||
| app := cli.NewApp() | ||
| app.Name = "check-jovian" | ||
| app.Usage = "Check Jovian upgrade results." | ||
| app.Description = "Check Jovian upgrade results." | ||
| app.Action = func(c *cli.Context) error { | ||
| return errors.New("see sub-commands") | ||
| } | ||
| app.Writer = os.Stdout | ||
| app.ErrWriter = os.Stderr | ||
| app.Commands = []*cli.Command{ | ||
| { | ||
| Name: "contracts", | ||
| Subcommands: []*cli.Command{ | ||
| makeCommand("gpo", checkGPO), | ||
| makeCommand("l1block", checkL1Block), | ||
| }, | ||
| }, | ||
| makeCommand("block", checkBlock), | ||
| makeCommand("extra-data", checkExtraData), | ||
| makeCommand("all", checkAll), | ||
| } | ||
|
|
||
| err := app.Run(os.Args) | ||
| if err != nil { | ||
| _, _ = fmt.Fprintf(os.Stderr, "Application failed: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
| } | ||
|
|
||
| type actionEnv struct { | ||
| log log.Logger | ||
| l2 *ethclient.Client | ||
| l2endpoint string | ||
| secretKey string | ||
| } | ||
|
|
||
| type CheckAction func(ctx context.Context, env *actionEnv) error | ||
|
|
||
| var ( | ||
| prefix = "CHECK_JOVIAN" | ||
| EndpointL2 = &cli.StringFlag{ | ||
| Name: "l2", | ||
| Usage: "L2 execution RPC endpoint", | ||
| EnvVars: op_service.PrefixEnvVar(prefix, "L2"), | ||
| Value: "http://localhost:9545", | ||
| } | ||
| SecretKeyFlag = &cli.StringFlag{ | ||
| Name: "secret-key", | ||
| Usage: "hex encoded secret key for sending a test tx (optional)", | ||
| EnvVars: op_service.PrefixEnvVar(prefix, "SECRET_KEY"), | ||
| Value: "", | ||
| } | ||
| ) | ||
|
|
||
| func makeFlags() []cli.Flag { | ||
| flags := []cli.Flag{ | ||
| EndpointL2, | ||
| SecretKeyFlag, | ||
| } | ||
| return append(flags, oplog.CLIFlags(prefix)...) | ||
| } | ||
|
|
||
| func makeCommand(name string, fn CheckAction) *cli.Command { | ||
| return &cli.Command{ | ||
| Name: name, | ||
| Action: makeCommandAction(fn), | ||
| Flags: cliapp.ProtectFlags(makeFlags()), | ||
| } | ||
| } | ||
|
|
||
| func makeCommandAction(fn CheckAction) func(c *cli.Context) error { | ||
| return func(c *cli.Context) error { | ||
| logCfg := oplog.ReadCLIConfig(c) | ||
| logger := oplog.NewLogger(c.App.Writer, logCfg) | ||
|
|
||
| c.Context = ctxinterrupt.WithCancelOnInterrupt(c.Context) | ||
| l2Cl, err := ethclient.DialContext(c.Context, c.String(EndpointL2.Name)) | ||
| if err != nil { | ||
sebastianst marked this conversation as resolved.
Show resolved
Hide resolved
sebastianst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return fmt.Errorf("failed to dial L2 RPC: %w", err) | ||
| } | ||
| secretKey := c.String(SecretKeyFlag.Name) | ||
| if secretKey != "" { | ||
| // Normalize possible 0x prefix | ||
| secretKey = strings.TrimPrefix(secretKey, "0x") | ||
| } | ||
sebastianst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if err := fn(c.Context, &actionEnv{ | ||
| log: logger, | ||
| l2: l2Cl, | ||
| l2endpoint: c.String(EndpointL2.Name), | ||
| secretKey: secretKey, | ||
| }); err != nil { | ||
| return fmt.Errorf("command error: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| // checkGPO checks that GasPriceOracle.isJovian returns true | ||
| func checkGPO(ctx context.Context, env *actionEnv) error { | ||
| cl, err := bindings.NewGasPriceOracle(predeploys.GasPriceOracleAddr, env.l2) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to create bindings around GasPriceOracle contract: %w", err) | ||
| } | ||
| isJovian, err := cl.IsJovian(nil) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get jovian status: %w", err) | ||
| } | ||
| if !isJovian { | ||
sebastianst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return fmt.Errorf("GPO is not set to jovian") | ||
| } | ||
| env.log.Info("GasPriceOracle test: success", "isJovian", isJovian) | ||
| return nil | ||
| } | ||
|
|
||
| // checkL1Block checks that L1Block.DAFootprintGasScalar returns a number | ||
| func checkL1Block(ctx context.Context, env *actionEnv) error { | ||
| cl, err := bindings.NewL1Block(predeploys.L1BlockAddr, env.l2) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to create bindings around L1Block contract: %w", err) | ||
| } | ||
| daFootprintGasScalar, err := cl.DaFootprintGasScalar(nil) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get DA footprint gas scalar from L1Block contract: %w", err) | ||
| } | ||
| if daFootprintGasScalar == 0 { | ||
| return fmt.Errorf("DA footprint gas scalar is set to 0 in L1Block contract, which should not be possible with Jovian.") | ||
| } | ||
| env.log.Info("L1Block test: success", "daFootprintGasScalar", daFootprintGasScalar) | ||
| return nil | ||
| } | ||
|
|
||
| // checkBlock checks that a block for correct use of a the blobgasused field. It can be inconclusive if | ||
| // there are no user transactions in the block. | ||
| // If a secret key is provided, it will attempt to send a tx-to-self on L2, wait for it to be mined, | ||
| // then use the block containing that tx as the block to check. | ||
| func checkBlock(ctx context.Context, env *actionEnv) error { | ||
| var err error | ||
| var latest *types.Block | ||
|
|
||
| // If a secret key was provided, attempt to send a tx-to-self and wait for it to be mined. | ||
| if env.secretKey != "" { | ||
| env.log.Info("secret key provided - attempting to send tx-to-self and wait for inclusion") | ||
|
|
||
| cfg := txmgr.NewCLIConfig(env.l2endpoint, txmgr.DefaultBatcherFlagValues) | ||
| cfg.PrivateKey = env.secretKey | ||
| t, err := txmgr.NewSimpleTxManager("check-jovian", env.log.With("component", "txmgr"), new(metrics.NoopTxMetrics), cfg) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to create tx manager: %w", err) | ||
| } | ||
| defer t.Close() | ||
| fromAddr := t.From() | ||
|
|
||
| receipt, err := t.Send(ctx, txmgr.TxCandidate{ | ||
| To: &fromAddr, // Send to self | ||
| Value: big.NewInt(0), | ||
| }) | ||
| if err != nil { | ||
| return fmt.Errorf("error waiting for tx to be mined: %w", err) | ||
| } | ||
| if receipt == nil { | ||
| return fmt.Errorf("tx mined receipt was nil") | ||
| } | ||
|
|
||
| env.log.Info("tx mined", "txHash", receipt.TxHash.Hex(), "blockNumber", receipt.BlockNumber.Uint64(), "blobGasUsed", receipt.BlobGasUsed) | ||
|
|
||
| if receipt.BlobGasUsed == 0 { | ||
| return fmt.Errorf("receipt.BlobGasUsed was zero (required with Jovian)") | ||
| } | ||
|
|
||
| // Fetch the block that contained the receipt | ||
| blk, err := env.l2.BlockByNumber(ctx, receipt.BlockNumber) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to fetch block containing tx: %w", err) | ||
| } | ||
| latest = blk | ||
| } else { | ||
| latest, err = env.l2.BlockByNumber(ctx, nil) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get latest block: %w", err) | ||
| } | ||
| } | ||
|
|
||
| bguPtr := latest.BlobGasUsed() | ||
| if bguPtr == nil { | ||
| return fmt.Errorf("block %d has nil BlobGasUsed field", latest.Number()) | ||
| } | ||
| bgu := *bguPtr | ||
|
|
||
| txs := latest.Body().Transactions | ||
| switch len(txs) { | ||
| case 0: | ||
| return fmt.Errorf("block %d has no transactions at all", latest.Number()) | ||
| case 1: | ||
| env.log.Warn("Block has no user txs - inconclusive for Jovian activation", | ||
| "blockNumber", latest.Number(), | ||
| "note", "Zero could indicate an empty block or pre-Jovian state") | ||
| default: | ||
| expectedDAFootprint, err := types.CalcDAFootprint(txs) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to calculate DA footprint for block %d: %w", latest.Number(), err) | ||
| } | ||
| if expectedDAFootprint != bgu { | ||
| return fmt.Errorf("expected DA footprint %d stored in header.blobGasUsed but got %d", expectedDAFootprint, bgu) | ||
| } | ||
| env.log.Info("Block header test: success - non-zero BlobGasUsed is hard evidence of Jovian being active", | ||
| "blockNumber", latest.Number, | ||
| "blobGasUsed", bgu, | ||
| "expectedDAFootprint", expectedDAFootprint) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // checkExtraData validates that the block header has the correct Jovian extra data format | ||
| func checkExtraData(ctx context.Context, env *actionEnv) error { | ||
| latest, err := env.l2.HeaderByNumber(ctx, nil) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get latest block: %w", err) | ||
| } | ||
|
|
||
| extra := latest.Extra | ||
|
|
||
| // Validate using op-geth's validation function | ||
| if err := eip1559.ValidateMinBaseFeeExtraData(extra); err != nil { | ||
| return fmt.Errorf("invalid extraData format: %w", err) | ||
| } | ||
|
|
||
| // Decode the validated extra data using op-geth's decode function | ||
| denominator, elasticity, minBaseFee := eip1559.DecodeMinBaseFeeExtraData(extra) | ||
|
|
||
| env.log.Info("ExtraData format test: success", | ||
| "blockNumber", latest.Number, | ||
| "version", extra[0], | ||
| "denominator", denominator, | ||
| "elasticity", elasticity, | ||
| "minBaseFee", *minBaseFee) | ||
| return nil | ||
| } | ||
|
|
||
| // checkAll runs all Jovian checks | ||
| func checkAll(ctx context.Context, env *actionEnv) error { | ||
| env.log.Info("starting Jovian checks") | ||
|
|
||
| if err := checkGPO(ctx, env); err != nil { | ||
| return fmt.Errorf("failed: GPO contract error: %w", err) | ||
| } | ||
| if err := checkL1Block(ctx, env); err != nil { | ||
| return fmt.Errorf("failed: L1Block contract error: %w", err) | ||
| } | ||
| if err := checkBlock(ctx, env); err != nil { | ||
| return fmt.Errorf("failed: block header error: %w", err) | ||
| } | ||
| if err := checkExtraData(ctx, env); err != nil { | ||
| return fmt.Errorf("failed: extra data format error: %w", err) | ||
| } | ||
|
|
||
| env.log.Info("completed all tests successfully!") | ||
|
|
||
| return nil | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.