Commit aba06fcf authored by Diego's avatar Diego Committed by GitHub

op-chain-ops: create tool to determine which contract versions are deployed for each chain (#8859)

* op-chain-ops: create op-version-check

forked from op-upgrade

* op-version-check: drop logic for upgrading contracts

* op-version-check: add logic for outputting contract versions

* op-version-check: improve support for checking multiple chains

* op-version-check: replace legacy upgrade references

* op-chain-ops: create README

* op-version-check: improve README

* op-version-check: drop unnecessary deploy-config flag

* op-version-check: create op-version-check in Makefile

* op-version-check: add usage section to README

* op-chain-ops: add op-version-check to README

* op-version-check: fix path in README

* op-version-check: remove unused function toDeployConfigName

* op-version-check: drop capitalization on error strings

* op-node: capitalize writeJSONFile function

this makes it accessible when op-node/cmd/genesis is imported as a package

* op-chain-ops: use op-node/cmd/genesis for writeJSON

* op-chain-ops: set name op_node_genesis for import

* op-chain-ops: use op-node WriteJSONFile in op-upgrade

* op-chains-ops: move to toSuperchainName to upgrades package

* op-chain-ops: output all contracts for op-version-check

addresses https://github.com/ethereum-optimism/optimism/pull/8859#issuecomment-1881663953

* op-chain-ops: add logging if chain ID is skipped by op-version-check

* op-service: create jsonutil/write

* op-chain-ops use jsonutil in op-upgrade, op-version-check

* op-chain-ops: pass RPC URLs as flags in op-version-check

* op-chain-ops: refactor of op-version-check

* op-chain-ops: refactor of op-version-check

* op-chain-ops: add more logging for op-version-check

* op-chain-ops: add more logging for op-version-check
parent 2ce0ccfd
op-version-check:
go build -o ./bin/op-version-check ./cmd/op-version-check/main.go
test: test:
go test ./... go test ./...
......
# op-chain-ops # op-chain-ops
This package contains utilities for working with chain state. This package contains utilities for working with chain state.
## op-version-check
A CLI tool for determining which contract versions are deployed for
chains in a superchain. It will output a JSON file that contains a
list of each chain's versions. It is assumed that the implementations
that are being checked have already been deployed and their contract
addresses exist inside of the `superchain-registry` repository. It is
also assumed that the semantic version file in the `superchain-registry`
has been updated. The tool will output the semantic versioning to
determine which contract versions are deployed.
### Configuration
#### L1 RPC URL
The L1 RPC URL is used to determine which superchain to target. All
L2s that are not based on top of the L1 chain that corresponds to the
L1 RPC URL are filtered out from being checked. It also is used to
double check that the data in the `superchain-registry` is correct.
#### Chain IDs
A list of L2 chain IDs can be passed that will be used to filter which
L2 chains will have their versions checked. Omitting this argument will
result in all chains in the superchain being considered.
#### Deploy Config
The path to the `deploy-config` directory in the contracts package.
Since multiple L2 networks may be considered in the check, the `deploy-config`
directory must be passed and then the particular deploy config files will
be read out of the directory as needed.
#### Outfile
The file that the versions should be written to. If omitted, the file
will be written to stdout
#### Usage
It can be built and run using the [Makefile](./Makefile) `op-version-check`
target. Run `make op-version-check` to create a binary in [./bin/op-version-check](./bin/op-version-check)
that can be executed, optionally providing the `--l1-rpc-url`, `--chain-ids`,
`--superchain-target`, and `--outfile` flags.
```sh
./bin/op-version-check
```
...@@ -18,6 +18,7 @@ import ( ...@@ -18,6 +18,7 @@ import (
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-chain-ops/safe" "github.com/ethereum-optimism/optimism/op-chain-ops/safe"
"github.com/ethereum-optimism/optimism/op-chain-ops/upgrades" "github.com/ethereum-optimism/optimism/op-chain-ops/upgrades"
"github.com/ethereum-optimism/optimism/op-service/jsonutil"
"github.com/ethereum-optimism/superchain-registry/superchain" "github.com/ethereum-optimism/superchain-registry/superchain"
) )
...@@ -79,7 +80,7 @@ func entrypoint(ctx *cli.Context) error { ...@@ -79,7 +80,7 @@ func entrypoint(ctx *cli.Context) error {
superchainName := ctx.String("superchain-target") superchainName := ctx.String("superchain-target")
if superchainName == "" { if superchainName == "" {
superchainName, err = toSuperchainName(l1ChainID.Uint64()) superchainName, err = upgrades.ToSuperchainName(l1ChainID.Uint64())
if err != nil { if err != nil {
return err return err
} }
...@@ -202,7 +203,7 @@ func entrypoint(ctx *cli.Context) error { ...@@ -202,7 +203,7 @@ func entrypoint(ctx *cli.Context) error {
// Write the batch to disk or stdout // Write the batch to disk or stdout
if outfile := ctx.Path("outfile"); outfile != "" { if outfile := ctx.Path("outfile"); outfile != "" {
if err := writeJSON(outfile, batch); err != nil { if err := jsonutil.WriteJSON(outfile, batch); err != nil {
return err return err
} }
} else { } else {
...@@ -240,30 +241,3 @@ func toDeployConfigName(cfg *superchain.ChainConfig) (string, error) { ...@@ -240,30 +241,3 @@ func toDeployConfigName(cfg *superchain.ChainConfig) (string, error) {
} }
return "", fmt.Errorf("unsupported chain name %s", cfg.Name) return "", fmt.Errorf("unsupported chain name %s", cfg.Name)
} }
// toSuperchainName turns a base layer chain id into a superchain
// network name.
func toSuperchainName(chainID uint64) (string, error) {
if chainID == 1 {
return "mainnet", nil
}
if chainID == 5 {
return "goerli", nil
}
if chainID == 11155111 {
return "sepolia", nil
}
return "", fmt.Errorf("unsupported chain ID %d", chainID)
}
func writeJSON(outfile string, input interface{}) error {
f, err := os.OpenFile(outfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
return enc.Encode(input)
}
# op-version-check
A CLI tool for determining which contract versions are deployed for
chains in a superchain. It will output a JSON file that contains a
list of each chain's versions. It is assumed that the implementations
that are being checked have already been deployed and their contract
addresses exist inside of the `superchain-registry` repository. It is
also assumed that the semantic version file in the `superchain-registry`
has been updated. The tool will output the semantic versioning to
determine which contract versions are deployed.
### Configuration
#### L1 RPC URL
The L1 RPC URL is used to determine which superchain to target. All
L2s that are not based on top of the L1 chain that corresponds to the
L1 RPC URL are filtered out from being checked. It also is used to
double check that the data in the `superchain-registry` is correct.
#### Chain IDs
A list of L2 chain IDs can be passed that will be used to filter which
L2 chains will have their versions checked. Omitting this argument will
result in all chains in the superchain being considered.
#### Deploy Config
The path to the `deploy-config` directory in the contracts package.
Since multiple L2 networks may be considered in the check, the `deploy-config`
directory must be passed and then the particular deploy config files will
be read out of the directory as needed.
#### Outfile
The file that the versions should be written to. If omitted, the file
will be written to stdout
#### Usage
It can be built and run using the [Makefile](../../Makefile) `op-version-check`
target. Run `make op-version-check` to create a binary in [../../bin/op-version-check](../../bin/op-version-check)
that can be executed, optionally providing the `--l1-rpc-url`, `--chain-ids`,
`--superchain-target`, and `--outfile` flags.
```sh
./bin/op-version-check
```
package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/mattn/go-isatty"
"github.com/urfave/cli/v2"
"golang.org/x/exp/maps"
"github.com/ethereum-optimism/optimism/op-chain-ops/upgrades"
"github.com/ethereum-optimism/optimism/op-service/jsonutil"
"github.com/ethereum-optimism/superchain-registry/superchain"
)
type Contract struct {
Version string `yaml:"version"`
Address superchain.Address `yaml:"address"`
}
type ChainVersionCheck struct {
Name string `yaml:"name"`
ChainID uint64 `yaml:"chain_id"`
Contracts map[string]Contract `yaml:"contracts"`
}
func main() {
log.Root().SetHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(isatty.IsTerminal(os.Stderr.Fd()))))
app := &cli.App{
Name: "op-version-check",
Usage: "Determine which contract versions are deployed for chains in a superchain",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "l1-rpc-urls",
Usage: "L1 RPC URLs, the chain ID will be used to determine the superchain",
EnvVars: []string{"L1_RPC_URL"},
},
&cli.StringSliceFlag{
Name: "l2-rpc-urls",
Usage: "L2 RPC URLs, corresponding to chains to check versions for. Corresponds to all chains if empty",
EnvVars: []string{"L1_RPC_URL"},
},
&cli.PathFlag{
Name: "outfile",
Usage: "The file to write the output to. If not specified, output is written to stdout",
EnvVars: []string{"OUTFILE"},
},
},
Action: entrypoint,
}
if err := app.Run(os.Args); err != nil {
log.Crit("error op-version-check", "err", err)
}
}
// entrypoint contains the main logic of the script
func entrypoint(ctx *cli.Context) error {
l1RPCURLs := ctx.StringSlice("l1-rpc-urls")
l2RPCURLs := ctx.StringSlice("l2-rpc-urls")
var l2ChainIDs []uint64
// If no L2 RPC URLs are specified, we check all chains for the L1 RPC URL
if len(l2RPCURLs) == 0 {
l2ChainIDs = maps.Keys(superchain.OPChains)
} else {
for _, l2RPCURL := range l2RPCURLs {
client, err := ethclient.Dial(l2RPCURL)
if err != nil {
return errors.New("cannot create L2 client")
}
l2ChainID, err := client.ChainID(ctx.Context)
if err != nil {
return fmt.Errorf("cannot fetch L2 chain ID: %w", err)
}
l2ChainIDs = append(l2ChainIDs, l2ChainID.Uint64())
}
}
output := []ChainVersionCheck{}
for _, l2ChainID := range l2ChainIDs {
chainConfig := superchain.OPChains[l2ChainID]
if chainConfig.ChainID != l2ChainID {
return fmt.Errorf("mismatched chain IDs: %d != %d", chainConfig.ChainID, l2ChainID)
}
for _, l1RPCURL := range l1RPCURLs {
client, err := ethclient.Dial(l1RPCURL)
if err != nil {
return errors.New("cannot create L1 client")
}
l1ChainID, err := client.ChainID(ctx.Context)
if err != nil {
return fmt.Errorf("cannot fetch L1 chain ID: %w", err)
}
superchainName, err := upgrades.ToSuperchainName(l1ChainID.Uint64())
if err != nil {
return fmt.Errorf("error getting superchain name: %w", err)
}
if superchainName != chainConfig.Superchain {
// L2 corresponds to a different superchain than L1, skip
log.Info("Ignoring L1/L2", "l1-chain-id", l1ChainID, "l2-chain-id", l2ChainID)
continue
}
log.Info(chainConfig.Name, "l1-chain-id", l1ChainID, "l2-chain-id", l2ChainID)
log.Info("Detecting on chain contracts")
// Tracking the individual addresses can be deprecated once the system is upgraded
// to the new contracts where the system config has a reference to each address.
addresses, ok := superchain.Addresses[l2ChainID]
if !ok {
return fmt.Errorf("no addresses for chain ID %d", l2ChainID)
}
versions, err := upgrades.GetContractVersions(ctx.Context, addresses, chainConfig, client)
if err != nil {
return fmt.Errorf("error getting contract versions: %w", err)
}
contracts := make(map[string]Contract)
contracts["AddressManager"] = Contract{Version: "null", Address: addresses.AddressManager}
contracts["L1CrossDomainMessenger"] = Contract{Version: versions.L1CrossDomainMessenger, Address: addresses.L1CrossDomainMessengerProxy}
contracts["L1ERC721Bridge"] = Contract{Version: versions.L1ERC721Bridge, Address: addresses.L1ERC721BridgeProxy}
contracts["L1StandardBridge"] = Contract{Version: versions.L1ERC721Bridge, Address: addresses.L1StandardBridgeProxy}
contracts["L2OutputOracle"] = Contract{Version: versions.L2OutputOracle, Address: addresses.L2OutputOracleProxy}
contracts["OptimismMintableERC20Factory"] = Contract{Version: versions.OptimismMintableERC20Factory, Address: addresses.OptimismMintableERC20FactoryProxy}
contracts["OptimismPortal"] = Contract{Version: versions.OptimismPortal, Address: addresses.OptimismPortalProxy}
contracts["SystemConfig"] = Contract{Version: versions.SystemConfig, Address: chainConfig.SystemConfigAddr}
contracts["ProxyAdmin"] = Contract{Version: "null", Address: addresses.ProxyAdmin}
output = append(output, ChainVersionCheck{Name: chainConfig.Name, ChainID: l2ChainID, Contracts: contracts})
log.Info("Successfully processed contract versions", "chain", chainConfig.Name, "l1-chain-id", l1ChainID, "l2-chain-id", l2ChainID)
break
}
}
// Write contract versions to disk or stdout
if outfile := ctx.Path("outfile"); outfile != "" {
if err := jsonutil.WriteJSON(outfile, output); err != nil {
return err
}
} else {
data, err := json.MarshalIndent(output, "", " ")
if err != nil {
return err
}
fmt.Println(string(data))
}
return nil
}
...@@ -122,3 +122,17 @@ func cmpVersion(v1, v2 string) bool { ...@@ -122,3 +122,17 @@ func cmpVersion(v1, v2 string) bool {
} }
return v1 == v2 return v1 == v2
} }
// ToSuperchainName turns a base layer chain id into a superchain network name.
func ToSuperchainName(chainID uint64) (string, error) {
if chainID == 1 {
return "mainnet", nil
}
if chainID == 5 {
return "goerli", nil
}
if chainID == 11155111 {
return "sepolia", nil
}
return "", fmt.Errorf("unsupported chain ID %d", chainID)
}
package jsonutil
import (
"encoding/json"
"os"
)
func WriteJSON(outfile string, input interface{}) error {
f, err := os.OpenFile(outfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
return enc.Encode(input)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment