Skip to content

Commit c4e6244

Browse files
geokneesebastianst
andauthored
op-chain-ops: add check-jovian cmd (#18269)
* op-chain-ops: add check-jovian cmd * op-chain-ops: add extra data format validation to check-jovian command * op-chain-ops: enhance block header validation in check-jovian command to differentiate between zero and non-zero BlobGasUsed * refactor using op-geth library fns * Rename checkBlockHeader to checkBlock and improve Jovian activation checks * 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. * lint * Require non-zero BlobGasUsed for Jovian * 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. * Validate BlobGasUsed against DA footprint * Use provided context for transaction send * Update op-chain-ops/cmd/check-jovian/main.go Co-authored-by: Sebastian Stammler <[email protected]> * Remove comment about BlobGasUsed * Document secret key option for check-jovian Add usage examples for CHECK_JOVIAN_SECRET env var and the --secret flag * 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. * Annotate txmgr logger with component field * 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. --------- Co-authored-by: Sebastian Stammler <[email protected]>
1 parent 43986f5 commit c4e6244

File tree

2 files changed

+378
-0
lines changed

2 files changed

+378
-0
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# check-jovian
2+
3+
A tool to verify that the Jovian upgrade has been successfully applied to an OP Stack chain.
4+
5+
## Overview
6+
7+
This tool checks four key aspects of the Jovian upgrade:
8+
9+
1. **GasPriceOracle Contract**: Verifies that `GasPriceOracle.isJovian()` returns `true`
10+
2. **L1Block Contract**: Verifies that `L1Block.DAFootprintGasScalar()` returns a valid number
11+
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)
12+
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)
13+
14+
## Usage
15+
16+
### Prerequisites
17+
18+
Set the L2 RPC endpoint via environment variable:
19+
```bash
20+
export CHECK_JOVIAN_L2=http://localhost:9545
21+
```
22+
23+
Or use the command-line flag:
24+
```bash
25+
--l2 http://localhost:9545
26+
```
27+
28+
To execute the most thorough checks, you may pass a secret key via the `CHECK_JOVIAN_SECRET` environment variable:
29+
```bash
30+
export CHECK_JOVIAN_SECRET=your-secret-key
31+
```
32+
33+
34+
Similarly, you can pass the secret key using the `--secret` flag:
35+
```bash
36+
--secret your-secret-key
37+
```
38+
39+
### Commands
40+
41+
#### Check all Jovian features
42+
```bash
43+
go run . all
44+
```
45+
46+
#### Check individual features
47+
48+
Check GasPriceOracle contract:
49+
```bash
50+
go run . contracts gpo
51+
```
52+
53+
Check L1Block contract:
54+
```bash
55+
go run . contracts l1block
56+
```
57+
58+
Check block header:
59+
```bash
60+
go run . block-header
61+
```
62+
63+
Check extra data format:
64+
```bash
65+
go run . extra-data
66+
```
67+
68+
## Build
69+
70+
From the `optimism` directory:
71+
```bash
72+
go build ./op-chain-ops/cmd/check-jovian
73+
```
74+
75+
## Implementation Details
76+
77+
The tool uses the `op-e2e/bindings` package to interact with the L2 contracts and verify:
78+
79+
- **GasPriceOracle.isJovian**: Returns `true` after the Jovian upgrade is activated
80+
- **L1Block.DAFootprintGasScalar**: Returns the DA footprint gas scalar value (warns if 0, as SystemConfig needs to update)
81+
- **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)
82+
- **Extra Data Format**: Validates the header `extraData` field contains:
83+
- 17 bytes total length
84+
- Version byte = 1 (Jovian version)
85+
- Denominator (uint32, bytes 1-5)
86+
- Elasticity (uint32, bytes 5-9)
87+
- Minimum Base Fee (uint64, bytes 9-17)
88+
89+
## Pattern
90+
91+
This tool follows the same pattern as `check-ecotone` and `check-fjord`, providing a systematic way to verify upgrade completion.
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"math/big"
8+
"os"
9+
"strings"
10+
11+
"github.com/urfave/cli/v2"
12+
13+
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
14+
"github.com/ethereum/go-ethereum/core/types"
15+
"github.com/ethereum/go-ethereum/ethclient"
16+
"github.com/ethereum/go-ethereum/log"
17+
18+
"github.com/ethereum-optimism/optimism/op-core/predeploys"
19+
"github.com/ethereum-optimism/optimism/op-e2e/bindings"
20+
op_service "github.com/ethereum-optimism/optimism/op-service"
21+
"github.com/ethereum-optimism/optimism/op-service/cliapp"
22+
"github.com/ethereum-optimism/optimism/op-service/ctxinterrupt"
23+
oplog "github.com/ethereum-optimism/optimism/op-service/log"
24+
"github.com/ethereum-optimism/optimism/op-service/txmgr"
25+
"github.com/ethereum-optimism/optimism/op-service/txmgr/metrics"
26+
)
27+
28+
func main() {
29+
app := cli.NewApp()
30+
app.Name = "check-jovian"
31+
app.Usage = "Check Jovian upgrade results."
32+
app.Description = "Check Jovian upgrade results."
33+
app.Action = func(c *cli.Context) error {
34+
return errors.New("see sub-commands")
35+
}
36+
app.Writer = os.Stdout
37+
app.ErrWriter = os.Stderr
38+
app.Commands = []*cli.Command{
39+
{
40+
Name: "contracts",
41+
Subcommands: []*cli.Command{
42+
makeCommand("gpo", checkGPO),
43+
makeCommand("l1block", checkL1Block),
44+
},
45+
},
46+
makeCommand("block", checkBlock),
47+
makeCommand("extra-data", checkExtraData),
48+
makeCommand("all", checkAll),
49+
}
50+
51+
err := app.Run(os.Args)
52+
if err != nil {
53+
_, _ = fmt.Fprintf(os.Stderr, "Application failed: %v\n", err)
54+
os.Exit(1)
55+
}
56+
}
57+
58+
type actionEnv struct {
59+
log log.Logger
60+
l2 *ethclient.Client
61+
l2endpoint string
62+
secretKey string
63+
}
64+
65+
type CheckAction func(ctx context.Context, env *actionEnv) error
66+
67+
var (
68+
prefix = "CHECK_JOVIAN"
69+
EndpointL2 = &cli.StringFlag{
70+
Name: "l2",
71+
Usage: "L2 execution RPC endpoint",
72+
EnvVars: op_service.PrefixEnvVar(prefix, "L2"),
73+
Value: "http://localhost:9545",
74+
}
75+
SecretKeyFlag = &cli.StringFlag{
76+
Name: "secret-key",
77+
Usage: "hex encoded secret key for sending a test tx (optional)",
78+
EnvVars: op_service.PrefixEnvVar(prefix, "SECRET_KEY"),
79+
Value: "",
80+
}
81+
)
82+
83+
func makeFlags() []cli.Flag {
84+
flags := []cli.Flag{
85+
EndpointL2,
86+
SecretKeyFlag,
87+
}
88+
return append(flags, oplog.CLIFlags(prefix)...)
89+
}
90+
91+
func makeCommand(name string, fn CheckAction) *cli.Command {
92+
return &cli.Command{
93+
Name: name,
94+
Action: makeCommandAction(fn),
95+
Flags: cliapp.ProtectFlags(makeFlags()),
96+
}
97+
}
98+
99+
func makeCommandAction(fn CheckAction) func(c *cli.Context) error {
100+
return func(c *cli.Context) error {
101+
logCfg := oplog.ReadCLIConfig(c)
102+
logger := oplog.NewLogger(c.App.Writer, logCfg)
103+
104+
c.Context = ctxinterrupt.WithCancelOnInterrupt(c.Context)
105+
l2Cl, err := ethclient.DialContext(c.Context, c.String(EndpointL2.Name))
106+
if err != nil {
107+
return fmt.Errorf("failed to dial L2 RPC: %w", err)
108+
}
109+
secretKey := c.String(SecretKeyFlag.Name)
110+
if secretKey != "" {
111+
// Normalize possible 0x prefix
112+
secretKey = strings.TrimPrefix(secretKey, "0x")
113+
}
114+
if err := fn(c.Context, &actionEnv{
115+
log: logger,
116+
l2: l2Cl,
117+
l2endpoint: c.String(EndpointL2.Name),
118+
secretKey: secretKey,
119+
}); err != nil {
120+
return fmt.Errorf("command error: %w", err)
121+
}
122+
return nil
123+
}
124+
}
125+
126+
// checkGPO checks that GasPriceOracle.isJovian returns true
127+
func checkGPO(ctx context.Context, env *actionEnv) error {
128+
cl, err := bindings.NewGasPriceOracle(predeploys.GasPriceOracleAddr, env.l2)
129+
if err != nil {
130+
return fmt.Errorf("failed to create bindings around GasPriceOracle contract: %w", err)
131+
}
132+
isJovian, err := cl.IsJovian(nil)
133+
if err != nil {
134+
return fmt.Errorf("failed to get jovian status: %w", err)
135+
}
136+
if !isJovian {
137+
return fmt.Errorf("GPO is not set to jovian")
138+
}
139+
env.log.Info("GasPriceOracle test: success", "isJovian", isJovian)
140+
return nil
141+
}
142+
143+
// checkL1Block checks that L1Block.DAFootprintGasScalar returns a number
144+
func checkL1Block(ctx context.Context, env *actionEnv) error {
145+
cl, err := bindings.NewL1Block(predeploys.L1BlockAddr, env.l2)
146+
if err != nil {
147+
return fmt.Errorf("failed to create bindings around L1Block contract: %w", err)
148+
}
149+
daFootprintGasScalar, err := cl.DaFootprintGasScalar(nil)
150+
if err != nil {
151+
return fmt.Errorf("failed to get DA footprint gas scalar from L1Block contract: %w", err)
152+
}
153+
if daFootprintGasScalar == 0 {
154+
return fmt.Errorf("DA footprint gas scalar is set to 0 in L1Block contract, which should not be possible with Jovian.")
155+
}
156+
env.log.Info("L1Block test: success", "daFootprintGasScalar", daFootprintGasScalar)
157+
return nil
158+
}
159+
160+
// checkBlock checks that a block for correct use of a the blobgasused field. It can be inconclusive if
161+
// there are no user transactions in the block.
162+
// If a secret key is provided, it will attempt to send a tx-to-self on L2, wait for it to be mined,
163+
// then use the block containing that tx as the block to check.
164+
func checkBlock(ctx context.Context, env *actionEnv) error {
165+
var err error
166+
var latest *types.Block
167+
168+
// If a secret key was provided, attempt to send a tx-to-self and wait for it to be mined.
169+
if env.secretKey != "" {
170+
env.log.Info("secret key provided - attempting to send tx-to-self and wait for inclusion")
171+
172+
cfg := txmgr.NewCLIConfig(env.l2endpoint, txmgr.DefaultBatcherFlagValues)
173+
cfg.PrivateKey = env.secretKey
174+
t, err := txmgr.NewSimpleTxManager("check-jovian", env.log.With("component", "txmgr"), new(metrics.NoopTxMetrics), cfg)
175+
if err != nil {
176+
return fmt.Errorf("failed to create tx manager: %w", err)
177+
}
178+
defer t.Close()
179+
fromAddr := t.From()
180+
181+
receipt, err := t.Send(ctx, txmgr.TxCandidate{
182+
To: &fromAddr, // Send to self
183+
Value: big.NewInt(0),
184+
})
185+
if err != nil {
186+
return fmt.Errorf("error waiting for tx to be mined: %w", err)
187+
}
188+
if receipt == nil {
189+
return fmt.Errorf("tx mined receipt was nil")
190+
}
191+
192+
env.log.Info("tx mined", "txHash", receipt.TxHash.Hex(), "blockNumber", receipt.BlockNumber.Uint64(), "blobGasUsed", receipt.BlobGasUsed)
193+
194+
if receipt.BlobGasUsed == 0 {
195+
return fmt.Errorf("receipt.BlobGasUsed was zero (required with Jovian)")
196+
}
197+
198+
// Fetch the block that contained the receipt
199+
blk, err := env.l2.BlockByNumber(ctx, receipt.BlockNumber)
200+
if err != nil {
201+
return fmt.Errorf("failed to fetch block containing tx: %w", err)
202+
}
203+
latest = blk
204+
} else {
205+
latest, err = env.l2.BlockByNumber(ctx, nil)
206+
if err != nil {
207+
return fmt.Errorf("failed to get latest block: %w", err)
208+
}
209+
}
210+
211+
bguPtr := latest.BlobGasUsed()
212+
if bguPtr == nil {
213+
return fmt.Errorf("block %d has nil BlobGasUsed field", latest.Number())
214+
}
215+
bgu := *bguPtr
216+
217+
txs := latest.Body().Transactions
218+
switch len(txs) {
219+
case 0:
220+
return fmt.Errorf("block %d has no transactions at all", latest.Number())
221+
case 1:
222+
env.log.Warn("Block has no user txs - inconclusive for Jovian activation",
223+
"blockNumber", latest.Number(),
224+
"note", "Zero could indicate an empty block or pre-Jovian state")
225+
default:
226+
expectedDAFootprint, err := types.CalcDAFootprint(txs)
227+
if err != nil {
228+
return fmt.Errorf("failed to calculate DA footprint for block %d: %w", latest.Number(), err)
229+
}
230+
if expectedDAFootprint != bgu {
231+
return fmt.Errorf("expected DA footprint %d stored in header.blobGasUsed but got %d", expectedDAFootprint, bgu)
232+
}
233+
env.log.Info("Block header test: success - non-zero BlobGasUsed is hard evidence of Jovian being active",
234+
"blockNumber", latest.Number,
235+
"blobGasUsed", bgu,
236+
"expectedDAFootprint", expectedDAFootprint)
237+
}
238+
return nil
239+
}
240+
241+
// checkExtraData validates that the block header has the correct Jovian extra data format
242+
func checkExtraData(ctx context.Context, env *actionEnv) error {
243+
latest, err := env.l2.HeaderByNumber(ctx, nil)
244+
if err != nil {
245+
return fmt.Errorf("failed to get latest block: %w", err)
246+
}
247+
248+
extra := latest.Extra
249+
250+
// Validate using op-geth's validation function
251+
if err := eip1559.ValidateMinBaseFeeExtraData(extra); err != nil {
252+
return fmt.Errorf("invalid extraData format: %w", err)
253+
}
254+
255+
// Decode the validated extra data using op-geth's decode function
256+
denominator, elasticity, minBaseFee := eip1559.DecodeMinBaseFeeExtraData(extra)
257+
258+
env.log.Info("ExtraData format test: success",
259+
"blockNumber", latest.Number,
260+
"version", extra[0],
261+
"denominator", denominator,
262+
"elasticity", elasticity,
263+
"minBaseFee", *minBaseFee)
264+
return nil
265+
}
266+
267+
// checkAll runs all Jovian checks
268+
func checkAll(ctx context.Context, env *actionEnv) error {
269+
env.log.Info("starting Jovian checks")
270+
271+
if err := checkGPO(ctx, env); err != nil {
272+
return fmt.Errorf("failed: GPO contract error: %w", err)
273+
}
274+
if err := checkL1Block(ctx, env); err != nil {
275+
return fmt.Errorf("failed: L1Block contract error: %w", err)
276+
}
277+
if err := checkBlock(ctx, env); err != nil {
278+
return fmt.Errorf("failed: block header error: %w", err)
279+
}
280+
if err := checkExtraData(ctx, env); err != nil {
281+
return fmt.Errorf("failed: extra data format error: %w", err)
282+
}
283+
284+
env.log.Info("completed all tests successfully!")
285+
286+
return nil
287+
}

0 commit comments

Comments
 (0)