From 3492c938bbec7c6d88e9972d4b56ce34c4796501 Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 13 Nov 2025 12:09:43 +0000 Subject: [PATCH 01/17] op-chain-ops: add check-jovian cmd --- op-chain-ops/cmd/check-jovian/README.md | 69 ++++++++++ op-chain-ops/cmd/check-jovian/main.go | 169 ++++++++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 op-chain-ops/cmd/check-jovian/README.md create mode 100644 op-chain-ops/cmd/check-jovian/main.go diff --git a/op-chain-ops/cmd/check-jovian/README.md b/op-chain-ops/cmd/check-jovian/README.md new file mode 100644 index 0000000000000..51263c14d33fd --- /dev/null +++ b/op-chain-ops/cmd/check-jovian/README.md @@ -0,0 +1,69 @@ +# check-jovian + +A tool to verify that the Jovian upgrade has been successfully applied to an OP Stack chain. + +## Overview + +This tool checks three 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 + +## 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 +``` + +### 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 +``` + +## 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 (used to track DA footprint limits) + +## Pattern + +This tool follows the same pattern as `check-ecotone` and `check-fjord`, providing a systematic way to verify upgrade completion. + diff --git a/op-chain-ops/cmd/check-jovian/main.go b/op-chain-ops/cmd/check-jovian/main.go new file mode 100644 index 0000000000000..2a063e8d7aa60 --- /dev/null +++ b/op-chain-ops/cmd/check-jovian/main.go @@ -0,0 +1,169 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/urfave/cli/v2" + + "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" +) + +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-header", checkBlockHeader), + 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 +} + +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", + } +) + +func makeFlags() []cli.Flag { + flags := []cli.Flag{ + EndpointL2, + } + 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 { + return fmt.Errorf("failed to dial L2 RPC: %w", err) + } + if err := fn(c.Context, &actionEnv{ + log: logger, + l2: l2Cl, + }); 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 { + 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 { + env.log.Warn("DA footprint gas scalar is set to 0. SystemConfig needs to emit scalar change to update.") + } + env.log.Info("L1Block test: success", "daFootprintGasScalar", daFootprintGasScalar) + return nil +} + +// checkBlockHeader checks that the latest block header has a non-nil blobgasused field +func checkBlockHeader(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) + } + if latest.BlobGasUsed == nil { + return fmt.Errorf("block %d has nil BlobGasUsed field", latest.Number) + } + env.log.Info("Block header test: success", + "blockNumber", latest.Number, + "blobGasUsed", *latest.BlobGasUsed) + 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 := checkBlockHeader(ctx, env); err != nil { + return fmt.Errorf("failed: block header error: %w", err) + } + + env.log.Info("completed all tests successfully!") + + return nil +} From fd70f960d5803a495e59f3874a3a2739ca901750 Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 13 Nov 2025 13:03:36 +0000 Subject: [PATCH 02/17] op-chain-ops: add extra data format validation to check-jovian command --- op-chain-ops/cmd/check-jovian/README.md | 14 +++++++- op-chain-ops/cmd/check-jovian/main.go | 47 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/op-chain-ops/cmd/check-jovian/README.md b/op-chain-ops/cmd/check-jovian/README.md index 51263c14d33fd..1fc2fd01bca68 100644 --- a/op-chain-ops/cmd/check-jovian/README.md +++ b/op-chain-ops/cmd/check-jovian/README.md @@ -4,11 +4,12 @@ A tool to verify that the Jovian upgrade has been successfully applied to an OP ## Overview -This tool checks three key aspects of the Jovian upgrade: +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 +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 @@ -48,6 +49,11 @@ Check block header: go run . block-header ``` +Check extra data format: +```bash +go run . extra-data +``` + ## Build From the `optimism` directory: @@ -62,6 +68,12 @@ The tool uses the `op-e2e/bindings` package to interact with the L2 contracts an - **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 (used to track DA footprint limits) +- **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 diff --git a/op-chain-ops/cmd/check-jovian/main.go b/op-chain-ops/cmd/check-jovian/main.go index 2a063e8d7aa60..5f3bc9f8abcb0 100644 --- a/op-chain-ops/cmd/check-jovian/main.go +++ b/op-chain-ops/cmd/check-jovian/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/binary" "errors" "fmt" "os" @@ -38,6 +39,7 @@ func main() { }, }, makeCommand("block-header", checkBlockHeader), + makeCommand("extra-data", checkExtraData), makeCommand("all", checkAll), } @@ -149,6 +151,48 @@ func checkBlockHeader(ctx context.Context, env *actionEnv) error { 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 + + // Check length - Jovian extraData must be 17 bytes + if len(extra) != 17 { + return fmt.Errorf("extraData should be 17 bytes for Jovian, got %d", len(extra)) + } + + // Check version byte - must be 1 for Jovian (incremented from Holocene's 0) + const JovianExtraDataVersionByte = uint8(0x01) + if extra[0] != JovianExtraDataVersionByte { + return fmt.Errorf("extraData version byte should be %d for Jovian, got %d", + JovianExtraDataVersionByte, extra[0]) + } + + // Decode EIP-1559 parameters (denominator and elasticity) + denominator := binary.BigEndian.Uint32(extra[1:5]) + elasticity := binary.BigEndian.Uint32(extra[5:9]) + + // Validate EIP-1559 params: denominator must be non-zero (unless elasticity is also 0) + if elasticity != 0 && denominator == 0 { + return fmt.Errorf("extraData has invalid EIP-1559 params: denominator cannot be 0 when elasticity is %d", elasticity) + } + + // Decode minimum base fee + minBaseFee := binary.BigEndian.Uint64(extra[9:17]) + + 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") @@ -162,6 +206,9 @@ func checkAll(ctx context.Context, env *actionEnv) error { if err := checkBlockHeader(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!") From e6834db6b33cb7ba09bbe7200a37bf2b4e7d28ef Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 13 Nov 2025 14:02:44 +0000 Subject: [PATCH 03/17] op-chain-ops: enhance block header validation in check-jovian command to differentiate between zero and non-zero BlobGasUsed --- op-chain-ops/cmd/check-jovian/README.md | 4 ++-- op-chain-ops/cmd/check-jovian/main.go | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/op-chain-ops/cmd/check-jovian/README.md b/op-chain-ops/cmd/check-jovian/README.md index 1fc2fd01bca68..de8ae461b504a 100644 --- a/op-chain-ops/cmd/check-jovian/README.md +++ b/op-chain-ops/cmd/check-jovian/README.md @@ -8,7 +8,7 @@ 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 +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 @@ -67,7 +67,7 @@ The tool uses the `op-e2e/bindings` package to interact with the L2 contracts an - **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 (used to track DA footprint limits) +- **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) diff --git a/op-chain-ops/cmd/check-jovian/main.go b/op-chain-ops/cmd/check-jovian/main.go index 5f3bc9f8abcb0..2162abf4138bd 100644 --- a/op-chain-ops/cmd/check-jovian/main.go +++ b/op-chain-ops/cmd/check-jovian/main.go @@ -145,9 +145,18 @@ func checkBlockHeader(ctx context.Context, env *actionEnv) error { if latest.BlobGasUsed == nil { return fmt.Errorf("block %d has nil BlobGasUsed field", latest.Number) } - env.log.Info("Block header test: success", - "blockNumber", latest.Number, - "blobGasUsed", *latest.BlobGasUsed) + + // A non-zero BlobGasUsed is hard evidence of Jovian being active + // A zero value is inconclusive (could be an empty block or pre-Jovian) + if *latest.BlobGasUsed == 0 { + env.log.Warn("Block header BlobGasUsed is zero - inconclusive for Jovian activation", + "blockNumber", latest.Number, + "note", "Zero could indicate an empty block or pre-Jovian state") + } else { + env.log.Info("Block header test: success - non-zero BlobGasUsed is hard evidence of Jovian being active", + "blockNumber", latest.Number, + "blobGasUsed", *latest.BlobGasUsed) + } return nil } From 8cca10c1163e76618dd2361f2ac96f675fafc8ac Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 13 Nov 2025 14:07:04 +0000 Subject: [PATCH 04/17] refactor using op-geth library fns --- op-chain-ops/cmd/check-jovian/main.go | 30 +++++++-------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/op-chain-ops/cmd/check-jovian/main.go b/op-chain-ops/cmd/check-jovian/main.go index 2162abf4138bd..c01f318d03f9b 100644 --- a/op-chain-ops/cmd/check-jovian/main.go +++ b/op-chain-ops/cmd/check-jovian/main.go @@ -2,13 +2,13 @@ package main import ( "context" - "encoding/binary" "errors" "fmt" "os" "github.com/urfave/cli/v2" + "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" @@ -169,36 +169,20 @@ func checkExtraData(ctx context.Context, env *actionEnv) error { extra := latest.Extra - // Check length - Jovian extraData must be 17 bytes - if len(extra) != 17 { - return fmt.Errorf("extraData should be 17 bytes for Jovian, got %d", len(extra)) + // Validate using op-geth's validation function + if err := eip1559.ValidateMinBaseFeeExtraData(extra); err != nil { + return fmt.Errorf("invalid extraData format: %w", err) } - // Check version byte - must be 1 for Jovian (incremented from Holocene's 0) - const JovianExtraDataVersionByte = uint8(0x01) - if extra[0] != JovianExtraDataVersionByte { - return fmt.Errorf("extraData version byte should be %d for Jovian, got %d", - JovianExtraDataVersionByte, extra[0]) - } - - // Decode EIP-1559 parameters (denominator and elasticity) - denominator := binary.BigEndian.Uint32(extra[1:5]) - elasticity := binary.BigEndian.Uint32(extra[5:9]) - - // Validate EIP-1559 params: denominator must be non-zero (unless elasticity is also 0) - if elasticity != 0 && denominator == 0 { - return fmt.Errorf("extraData has invalid EIP-1559 params: denominator cannot be 0 when elasticity is %d", elasticity) - } - - // Decode minimum base fee - minBaseFee := binary.BigEndian.Uint64(extra[9:17]) + // 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) + "minBaseFee", *minBaseFee) return nil } From 5744d9df032c8918e45a53f13aee1902f4b4d5ba Mon Sep 17 00:00:00 2001 From: geoknee Date: Wed, 19 Nov 2025 13:26:33 +0000 Subject: [PATCH 05/17] Rename checkBlockHeader to checkBlock and improve Jovian activation checks --- op-chain-ops/cmd/check-jovian/main.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/op-chain-ops/cmd/check-jovian/main.go b/op-chain-ops/cmd/check-jovian/main.go index c01f318d03f9b..220f81240cd4b 100644 --- a/op-chain-ops/cmd/check-jovian/main.go +++ b/op-chain-ops/cmd/check-jovian/main.go @@ -9,6 +9,7 @@ import ( "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" @@ -38,7 +39,7 @@ func main() { makeCommand("l1block", checkL1Block), }, }, - makeCommand("block-header", checkBlockHeader), + makeCommand("block", checkBlock), makeCommand("extra-data", checkExtraData), makeCommand("all", checkAll), } @@ -136,26 +137,31 @@ func checkL1Block(ctx context.Context, env *actionEnv) error { return nil } -// checkBlockHeader checks that the latest block header has a non-nil blobgasused field -func checkBlockHeader(ctx context.Context, env *actionEnv) error { - latest, err := env.l2.HeaderByNumber(ctx, nil) +// checkBlock checks that the latest block header has a non-nil blobgasused field +func checkBlock(ctx context.Context, env *actionEnv) error { + latest, err := env.l2.BlockByNumber(ctx, nil) if err != nil { return fmt.Errorf("failed to get latest block: %w", err) } - if latest.BlobGasUsed == nil { + bgu := latest.BlobGasUsed() + if bgu == nil { return fmt.Errorf("block %d has nil BlobGasUsed field", latest.Number) } // A non-zero BlobGasUsed is hard evidence of Jovian being active // A zero value is inconclusive (could be an empty block or pre-Jovian) - if *latest.BlobGasUsed == 0 { + if *bgu == 0 { env.log.Warn("Block header BlobGasUsed is zero - inconclusive for Jovian activation", "blockNumber", latest.Number, "note", "Zero could indicate an empty block or pre-Jovian state") + txs := latest.Body().Transactions + if len(txs) > 1 && txs[len(txs)-1].Type() == types.DepositTxType { + return fmt.Errorf("User transactions in block but header.BlobGasUsed was zero, impossible if Jovian is active.") + } } else { env.log.Info("Block header test: success - non-zero BlobGasUsed is hard evidence of Jovian being active", "blockNumber", latest.Number, - "blobGasUsed", *latest.BlobGasUsed) + "blobGasUsed", bgu) } return nil } @@ -196,7 +202,7 @@ func checkAll(ctx context.Context, env *actionEnv) error { if err := checkL1Block(ctx, env); err != nil { return fmt.Errorf("failed: L1Block contract error: %w", err) } - if err := checkBlockHeader(ctx, env); err != nil { + if err := checkBlock(ctx, env); err != nil { return fmt.Errorf("failed: block header error: %w", err) } if err := checkExtraData(ctx, env); err != nil { From 3a283ebc3262166475175ff70c8d256d30cf4122 Mon Sep 17 00:00:00 2001 From: geoknee Date: Wed, 19 Nov 2025 14:00:25 +0000 Subject: [PATCH 06/17] Add secret-key flag to send tx-to-self When a hex secret key is supplied, send a signed tx-to-self on L2, wait up to 2 minutes for it to be mined, and use its block for the BlobGasUsed check. Normalize 0x prefix and surface errors on failure. --- op-chain-ops/cmd/check-jovian/main.go | 109 ++++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 7 deletions(-) diff --git a/op-chain-ops/cmd/check-jovian/main.go b/op-chain-ops/cmd/check-jovian/main.go index 220f81240cd4b..73050e52b858e 100644 --- a/op-chain-ops/cmd/check-jovian/main.go +++ b/op-chain-ops/cmd/check-jovian/main.go @@ -4,12 +4,17 @@ import ( "context" "errors" "fmt" + "math/big" "os" + "strings" + "time" "github.com/urfave/cli/v2" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" @@ -52,8 +57,9 @@ func main() { } type actionEnv struct { - log log.Logger - l2 *ethclient.Client + log log.Logger + l2 *ethclient.Client + secretKey string } type CheckAction func(ctx context.Context, env *actionEnv) error @@ -66,11 +72,18 @@ var ( 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)...) } @@ -93,9 +106,15 @@ func makeCommandAction(fn CheckAction) func(c *cli.Context) error { if err != nil { 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") + } if err := fn(c.Context, &actionEnv{ - log: logger, - l2: l2Cl, + log: logger, + l2: l2Cl, + secretKey: secretKey, }); err != nil { return fmt.Errorf("command error: %w", err) } @@ -138,11 +157,87 @@ func checkL1Block(ctx context.Context, env *actionEnv) error { } // checkBlock checks that the latest block header has a non-nil blobgasused field +// 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 { - latest, err := env.l2.BlockByNumber(ctx, nil) - if err != nil { - return fmt.Errorf("failed to get latest block: %w", err) + var latest *types.Block + var err error + + // 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") + + // Parse private key + priv, err := crypto.HexToECDSA(env.secretKey) + if err != nil { + return fmt.Errorf("failed to parse secret key: %w", err) + } + fromAddr := crypto.PubkeyToAddress(priv.PublicKey) + + // Get nonce + nonce, err := env.l2.PendingNonceAt(ctx, fromAddr) + if err != nil { + return fmt.Errorf("failed to get pending nonce: %w", err) + } + + // Gas price + gasPrice, err := env.l2.SuggestGasPrice(ctx) + if err != nil { + return fmt.Errorf("failed to suggest gas price: %w", err) + } + + // Simple to-self transfer with zero value (no data). Use a minimal gas limit. + toAddr := fromAddr + value := big.NewInt(0) + gasLimit := uint64(21000) + + // Determine chain ID for signing + chainID, err := env.l2.NetworkID(ctx) + if err != nil { + return fmt.Errorf("failed to get network id: %w", err) + } + + tx := types.NewTransaction(nonce, toAddr, value, gasLimit, gasPrice, nil) + + // Sign tx + signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), priv) + if err != nil { + return fmt.Errorf("failed to sign tx: %w", err) + } + + // Send tx + if err := env.l2.SendTransaction(ctx, signedTx); err != nil { + return fmt.Errorf("failed to send tx: %w", err) + } + env.log.Info("tx sent", "txHash", signedTx.Hash().Hex(), "from", fromAddr.Hex()) + + // Wait for mined receipt (with a reasonable timeout) + // Use a context with timeout to avoid waiting forever. + waitCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) + defer cancel() + + receipt, err := bind.WaitMined(waitCtx, env.l2, signedTx) + 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", signedTx.Hash().Hex(), "blockNumber", receipt.BlockNumber) + + // 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) + } } + bgu := latest.BlobGasUsed() if bgu == nil { return fmt.Errorf("block %d has nil BlobGasUsed field", latest.Number) From 8d25fd0a13dcada04fa1dc014aafba2560a3bd54 Mon Sep 17 00:00:00 2001 From: geoknee Date: Wed, 19 Nov 2025 14:07:56 +0000 Subject: [PATCH 07/17] lint --- op-chain-ops/cmd/check-jovian/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/op-chain-ops/cmd/check-jovian/main.go b/op-chain-ops/cmd/check-jovian/main.go index 73050e52b858e..3b91594120d49 100644 --- a/op-chain-ops/cmd/check-jovian/main.go +++ b/op-chain-ops/cmd/check-jovian/main.go @@ -240,14 +240,14 @@ func checkBlock(ctx context.Context, env *actionEnv) error { bgu := latest.BlobGasUsed() if bgu == nil { - return fmt.Errorf("block %d has nil BlobGasUsed field", latest.Number) + return fmt.Errorf("block %d has nil BlobGasUsed field", latest.Number()) } // A non-zero BlobGasUsed is hard evidence of Jovian being active // A zero value is inconclusive (could be an empty block or pre-Jovian) if *bgu == 0 { env.log.Warn("Block header BlobGasUsed is zero - inconclusive for Jovian activation", - "blockNumber", latest.Number, + "blockNumber", latest.Number(), "note", "Zero could indicate an empty block or pre-Jovian state") txs := latest.Body().Transactions if len(txs) > 1 && txs[len(txs)-1].Type() == types.DepositTxType { From 0f3c7698209a537d9387345d5335a23c53a0b0cc Mon Sep 17 00:00:00 2001 From: geoknee Date: Wed, 19 Nov 2025 14:14:02 +0000 Subject: [PATCH 08/17] Require non-zero BlobGasUsed for Jovian --- op-chain-ops/cmd/check-jovian/main.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/op-chain-ops/cmd/check-jovian/main.go b/op-chain-ops/cmd/check-jovian/main.go index 3b91594120d49..8d94cc38cd472 100644 --- a/op-chain-ops/cmd/check-jovian/main.go +++ b/op-chain-ops/cmd/check-jovian/main.go @@ -223,7 +223,11 @@ func checkBlock(ctx context.Context, env *actionEnv) error { if receipt == nil { return fmt.Errorf("tx mined receipt was nil") } - env.log.Info("tx mined", "txHash", signedTx.Hash().Hex(), "blockNumber", receipt.BlockNumber) + env.log.Info("tx mined", "txHash", signedTx.Hash().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) From 56e27d955416f451a7c03efacada7c58dabd38f6 Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 20 Nov 2025 12:34:55 +0000 Subject: [PATCH 09/17] Use txmgr to send and wait for tx in check-jovian Replace manual key parsing, signing, and bind.WaitMined with a SimpleTxManager (txmgr). Add l2endpoint to env and wire txmgr config, using txmgr.Send to submit and await the self-transfer receipt. --- op-chain-ops/cmd/check-jovian/main.go | 81 ++++++++------------------- 1 file changed, 24 insertions(+), 57 deletions(-) diff --git a/op-chain-ops/cmd/check-jovian/main.go b/op-chain-ops/cmd/check-jovian/main.go index 8d94cc38cd472..bb5a8a5e18d9e 100644 --- a/op-chain-ops/cmd/check-jovian/main.go +++ b/op-chain-ops/cmd/check-jovian/main.go @@ -7,14 +7,11 @@ import ( "math/big" "os" "strings" - "time" "github.com/urfave/cli/v2" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" @@ -24,6 +21,8 @@ import ( "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() { @@ -57,9 +56,10 @@ func main() { } type actionEnv struct { - log log.Logger - l2 *ethclient.Client - secretKey string + log log.Logger + l2 *ethclient.Client + l2endpoint string + secretKey string } type CheckAction func(ctx context.Context, env *actionEnv) error @@ -112,9 +112,10 @@ func makeCommandAction(fn CheckAction) func(c *cli.Context) error { secretKey = strings.TrimPrefix(secretKey, "0x") } if err := fn(c.Context, &actionEnv{ - log: logger, - l2: l2Cl, - secretKey: secretKey, + log: logger, + l2: l2Cl, + l2endpoint: c.String(EndpointL2.Name), + secretKey: secretKey, }); err != nil { return fmt.Errorf("command error: %w", err) } @@ -160,70 +161,36 @@ func checkL1Block(ctx context.Context, env *actionEnv) error { // 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 latest *types.Block 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") - // Parse private key - priv, err := crypto.HexToECDSA(env.secretKey) - if err != nil { - return fmt.Errorf("failed to parse secret key: %w", err) - } - fromAddr := crypto.PubkeyToAddress(priv.PublicKey) - - // Get nonce - nonce, err := env.l2.PendingNonceAt(ctx, fromAddr) - if err != nil { - return fmt.Errorf("failed to get pending nonce: %w", err) - } - - // Gas price - gasPrice, err := env.l2.SuggestGasPrice(ctx) - if err != nil { - return fmt.Errorf("failed to suggest gas price: %w", err) - } - - // Simple to-self transfer with zero value (no data). Use a minimal gas limit. - toAddr := fromAddr - value := big.NewInt(0) - gasLimit := uint64(21000) - - // Determine chain ID for signing - chainID, err := env.l2.NetworkID(ctx) - if err != nil { - return fmt.Errorf("failed to get network id: %w", err) - } - - tx := types.NewTransaction(nonce, toAddr, value, gasLimit, gasPrice, nil) + cfg := txmgr.NewCLIConfig(env.l2endpoint, txmgr.DefaultBatcherFlagValues) + cfg.PrivateKey = env.secretKey + t, err := txmgr.NewSimpleTxManager("check-jovian", env.log, new(metrics.NoopTxMetrics), cfg) + fromAddr := t.From() - // Sign tx - signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), priv) if err != nil { - return fmt.Errorf("failed to sign tx: %w", err) - } - - // Send tx - if err := env.l2.SendTransaction(ctx, signedTx); err != nil { - return fmt.Errorf("failed to send tx: %w", err) + return fmt.Errorf("failed to create tx manager: %w", err) } - env.log.Info("tx sent", "txHash", signedTx.Hash().Hex(), "from", fromAddr.Hex()) + defer t.Close() - // Wait for mined receipt (with a reasonable timeout) - // Use a context with timeout to avoid waiting forever. - waitCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) - defer cancel() - - receipt, err := bind.WaitMined(waitCtx, env.l2, signedTx) + receipt, err := t.Send(context.TODO(), 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", signedTx.Hash().Hex(), "blockNumber", receipt.BlockNumber.Uint64(), "blobGasUsed", receipt.BlobGasUsed) + + 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)") From 56b91603f385d9f20d01b80042332c78399371c2 Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 20 Nov 2025 12:39:00 +0000 Subject: [PATCH 10/17] Validate BlobGasUsed against DA footprint --- op-chain-ops/cmd/check-jovian/main.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/op-chain-ops/cmd/check-jovian/main.go b/op-chain-ops/cmd/check-jovian/main.go index bb5a8a5e18d9e..328f9df024427 100644 --- a/op-chain-ops/cmd/check-jovian/main.go +++ b/op-chain-ops/cmd/check-jovian/main.go @@ -214,20 +214,30 @@ func checkBlock(ctx context.Context, env *actionEnv) error { return fmt.Errorf("block %d has nil BlobGasUsed field", latest.Number()) } + txs := latest.Body().Transactions + // A non-zero BlobGasUsed is hard evidence of Jovian being active // A zero value is inconclusive (could be an empty block or pre-Jovian) if *bgu == 0 { env.log.Warn("Block header BlobGasUsed is zero - inconclusive for Jovian activation", "blockNumber", latest.Number(), "note", "Zero could indicate an empty block or pre-Jovian state") - txs := latest.Body().Transactions + if len(txs) > 1 && txs[len(txs)-1].Type() == types.DepositTxType { return fmt.Errorf("User transactions in block but header.BlobGasUsed was zero, impossible if Jovian is active.") } } else { + 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) + "blobGasUsed", *bgu, + "expectedDAFootprint", expectedDAFootprint) } return nil } From d387ddec29d08f0c7b4b16911750c58a77f60652 Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 20 Nov 2025 12:42:36 +0000 Subject: [PATCH 11/17] Use provided context for transaction send --- op-chain-ops/cmd/check-jovian/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op-chain-ops/cmd/check-jovian/main.go b/op-chain-ops/cmd/check-jovian/main.go index 328f9df024427..be1d291085846 100644 --- a/op-chain-ops/cmd/check-jovian/main.go +++ b/op-chain-ops/cmd/check-jovian/main.go @@ -179,7 +179,7 @@ func checkBlock(ctx context.Context, env *actionEnv) error { } defer t.Close() - receipt, err := t.Send(context.TODO(), txmgr.TxCandidate{ + receipt, err := t.Send(ctx, txmgr.TxCandidate{ To: &fromAddr, // Send to self Value: big.NewInt(0), }) From 8e8027637bf7b51385b94a8ddc8297cb642578c8 Mon Sep 17 00:00:00 2001 From: George Knee Date: Thu, 20 Nov 2025 13:22:47 +0000 Subject: [PATCH 12/17] Update op-chain-ops/cmd/check-jovian/main.go Co-authored-by: Sebastian Stammler --- op-chain-ops/cmd/check-jovian/main.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/op-chain-ops/cmd/check-jovian/main.go b/op-chain-ops/cmd/check-jovian/main.go index be1d291085846..1a8fed16e84a1 100644 --- a/op-chain-ops/cmd/check-jovian/main.go +++ b/op-chain-ops/cmd/check-jovian/main.go @@ -218,14 +218,10 @@ func checkBlock(ctx context.Context, env *actionEnv) error { // A non-zero BlobGasUsed is hard evidence of Jovian being active // A zero value is inconclusive (could be an empty block or pre-Jovian) - if *bgu == 0 { - env.log.Warn("Block header BlobGasUsed is zero - inconclusive for Jovian activation", + if len(txs) == 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") - - if len(txs) > 1 && txs[len(txs)-1].Type() == types.DepositTxType { - return fmt.Errorf("User transactions in block but header.BlobGasUsed was zero, impossible if Jovian is active.") - } } else { expectedDAFootprint, err := types.CalcDAFootprint(txs) if err != nil { From 5b97b0ce3fe96a6bc62f54bdb6b942a001013db9 Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 20 Nov 2025 13:24:42 +0000 Subject: [PATCH 13/17] Remove comment about BlobGasUsed --- op-chain-ops/cmd/check-jovian/main.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/op-chain-ops/cmd/check-jovian/main.go b/op-chain-ops/cmd/check-jovian/main.go index 1a8fed16e84a1..273decaf2df31 100644 --- a/op-chain-ops/cmd/check-jovian/main.go +++ b/op-chain-ops/cmd/check-jovian/main.go @@ -215,9 +215,6 @@ func checkBlock(ctx context.Context, env *actionEnv) error { } txs := latest.Body().Transactions - - // A non-zero BlobGasUsed is hard evidence of Jovian being active - // A zero value is inconclusive (could be an empty block or pre-Jovian) if len(txs) == 1 { env.log.Warn("Block has no user txs - inconclusive for Jovian activation", "blockNumber", latest.Number(), From 51ab4bc9af0da00cf49d3d157ca5ea9268c2ca65 Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 20 Nov 2025 13:27:51 +0000 Subject: [PATCH 14/17] Document secret key option for check-jovian Add usage examples for CHECK_JOVIAN_SECRET env var and the --secret flag --- op-chain-ops/cmd/check-jovian/README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/op-chain-ops/cmd/check-jovian/README.md b/op-chain-ops/cmd/check-jovian/README.md index de8ae461b504a..5a260ce156cf3 100644 --- a/op-chain-ops/cmd/check-jovian/README.md +++ b/op-chain-ops/cmd/check-jovian/README.md @@ -25,6 +25,17 @@ Or use the command-line flag: --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 @@ -78,4 +89,3 @@ The tool uses the `op-e2e/bindings` package to interact with the L2 contracts an ## Pattern This tool follows the same pattern as `check-ecotone` and `check-fjord`, providing a systematic way to verify upgrade completion. - From ab394b9d92f0c38582a958be8d88d38079f81bef Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 20 Nov 2025 13:33:17 +0000 Subject: [PATCH 15/17] Return error on zero DA scalar; harden blobGasUsed Treat a DA footprint gas scalar of 0 in L1Block as an error (Jovian should not allow it). Delay calling t.From() until after creating the tx manager and check the BlobGasUsed pointer before dereferencing to avoid nil derefs. --- op-chain-ops/cmd/check-jovian/main.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/op-chain-ops/cmd/check-jovian/main.go b/op-chain-ops/cmd/check-jovian/main.go index 273decaf2df31..179a40fef3010 100644 --- a/op-chain-ops/cmd/check-jovian/main.go +++ b/op-chain-ops/cmd/check-jovian/main.go @@ -151,18 +151,18 @@ func checkL1Block(ctx context.Context, env *actionEnv) error { return fmt.Errorf("failed to get DA footprint gas scalar from L1Block contract: %w", err) } if daFootprintGasScalar == 0 { - env.log.Warn("DA footprint gas scalar is set to 0. SystemConfig needs to emit scalar change to update.") + 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 the latest block header has a non-nil blobgasused field +// 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. @@ -172,12 +172,11 @@ func checkBlock(ctx context.Context, env *actionEnv) error { cfg := txmgr.NewCLIConfig(env.l2endpoint, txmgr.DefaultBatcherFlagValues) cfg.PrivateKey = env.secretKey t, err := txmgr.NewSimpleTxManager("check-jovian", env.log, new(metrics.NoopTxMetrics), cfg) - fromAddr := t.From() - 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 @@ -209,10 +208,11 @@ func checkBlock(ctx context.Context, env *actionEnv) error { } } - bgu := latest.BlobGasUsed() - if bgu == nil { + bguPtr := latest.BlobGasUsed() + if bguPtr == nil { return fmt.Errorf("block %d has nil BlobGasUsed field", latest.Number()) } + bgu := *bguPtr txs := latest.Body().Transactions if len(txs) == 1 { @@ -224,12 +224,12 @@ func checkBlock(ctx context.Context, env *actionEnv) error { 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) + 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, + "blobGasUsed", bgu, "expectedDAFootprint", expectedDAFootprint) } return nil From a2ac9273ecd1d6a29b444c68f165afcc43d21241 Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 20 Nov 2025 13:37:58 +0000 Subject: [PATCH 16/17] Annotate txmgr logger with component field --- op-chain-ops/cmd/check-jovian/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op-chain-ops/cmd/check-jovian/main.go b/op-chain-ops/cmd/check-jovian/main.go index 179a40fef3010..1bd16e37ff0d6 100644 --- a/op-chain-ops/cmd/check-jovian/main.go +++ b/op-chain-ops/cmd/check-jovian/main.go @@ -171,7 +171,7 @@ func checkBlock(ctx context.Context, env *actionEnv) error { cfg := txmgr.NewCLIConfig(env.l2endpoint, txmgr.DefaultBatcherFlagValues) cfg.PrivateKey = env.secretKey - t, err := txmgr.NewSimpleTxManager("check-jovian", env.log, new(metrics.NoopTxMetrics), cfg) + 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) } From d4a798f8105f339c43fc2d9d954b6cc340535051 Mon Sep 17 00:00:00 2001 From: geoknee Date: Thu, 20 Nov 2025 13:41:41 +0000 Subject: [PATCH 17/17] Return error for blocks with no transactions Treat blocks with zero transactions as an error instead of proceeding. Retain the inconclusive warning for single-transaction blocks. Compute DA footprint only for blocks with multiple transactions. --- op-chain-ops/cmd/check-jovian/main.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/op-chain-ops/cmd/check-jovian/main.go b/op-chain-ops/cmd/check-jovian/main.go index 1bd16e37ff0d6..4bf7e7d258391 100644 --- a/op-chain-ops/cmd/check-jovian/main.go +++ b/op-chain-ops/cmd/check-jovian/main.go @@ -215,11 +215,14 @@ func checkBlock(ctx context.Context, env *actionEnv) error { bgu := *bguPtr txs := latest.Body().Transactions - if len(txs) == 1 { + 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") - } else { + default: expectedDAFootprint, err := types.CalcDAFootprint(txs) if err != nil { return fmt.Errorf("failed to calculate DA footprint for block %d: %w", latest.Number(), err)