From aba06fcfa6d64b49fcb36cd4abf0045e1f4daeb1 Mon Sep 17 00:00:00 2001 From: Diego <105765223+0xfuturistic@users.noreply.github.com> Date: Wed, 17 Jan 2024 19:28:22 -0500 Subject: [PATCH] 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 --- op-chain-ops/Makefile | 3 + op-chain-ops/README.md | 49 ++++++ op-chain-ops/cmd/op-upgrade/main.go | 32 +--- op-chain-ops/cmd/op-version-check/README.md | 48 ++++++ op-chain-ops/cmd/op-version-check/main.go | 164 ++++++++++++++++++++ op-chain-ops/upgrades/check.go | 14 ++ op-service/jsonutil/write.go | 18 +++ 7 files changed, 299 insertions(+), 29 deletions(-) create mode 100644 op-chain-ops/cmd/op-version-check/README.md create mode 100644 op-chain-ops/cmd/op-version-check/main.go create mode 100644 op-service/jsonutil/write.go diff --git a/op-chain-ops/Makefile b/op-chain-ops/Makefile index 07c2b60d1897..bcd56a2a159f 100644 --- a/op-chain-ops/Makefile +++ b/op-chain-ops/Makefile @@ -1,3 +1,6 @@ +op-version-check: + go build -o ./bin/op-version-check ./cmd/op-version-check/main.go + test: go test ./... diff --git a/op-chain-ops/README.md b/op-chain-ops/README.md index c0ad18b39936..a40232fe7d17 100644 --- a/op-chain-ops/README.md +++ b/op-chain-ops/README.md @@ -1,3 +1,52 @@ # op-chain-ops 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 +``` diff --git a/op-chain-ops/cmd/op-upgrade/main.go b/op-chain-ops/cmd/op-upgrade/main.go index 7fdf67c8967e..164f27465f72 100644 --- a/op-chain-ops/cmd/op-upgrade/main.go +++ b/op-chain-ops/cmd/op-upgrade/main.go @@ -18,6 +18,7 @@ import ( "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/upgrades" + "github.com/ethereum-optimism/optimism/op-service/jsonutil" "github.com/ethereum-optimism/superchain-registry/superchain" ) @@ -79,7 +80,7 @@ func entrypoint(ctx *cli.Context) error { superchainName := ctx.String("superchain-target") if superchainName == "" { - superchainName, err = toSuperchainName(l1ChainID.Uint64()) + superchainName, err = upgrades.ToSuperchainName(l1ChainID.Uint64()) if err != nil { return err } @@ -202,7 +203,7 @@ func entrypoint(ctx *cli.Context) error { // Write the batch to disk or stdout if outfile := ctx.Path("outfile"); outfile != "" { - if err := writeJSON(outfile, batch); err != nil { + if err := jsonutil.WriteJSON(outfile, batch); err != nil { return err } } else { @@ -240,30 +241,3 @@ func toDeployConfigName(cfg *superchain.ChainConfig) (string, error) { } 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) -} diff --git a/op-chain-ops/cmd/op-version-check/README.md b/op-chain-ops/cmd/op-version-check/README.md new file mode 100644 index 000000000000..29e5f08ab4de --- /dev/null +++ b/op-chain-ops/cmd/op-version-check/README.md @@ -0,0 +1,48 @@ +# 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 +``` diff --git a/op-chain-ops/cmd/op-version-check/main.go b/op-chain-ops/cmd/op-version-check/main.go new file mode 100644 index 000000000000..1109b092968d --- /dev/null +++ b/op-chain-ops/cmd/op-version-check/main.go @@ -0,0 +1,164 @@ +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 +} diff --git a/op-chain-ops/upgrades/check.go b/op-chain-ops/upgrades/check.go index 95cd3f770535..6239c23a1451 100644 --- a/op-chain-ops/upgrades/check.go +++ b/op-chain-ops/upgrades/check.go @@ -122,3 +122,17 @@ func cmpVersion(v1, v2 string) bool { } 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) +} diff --git a/op-service/jsonutil/write.go b/op-service/jsonutil/write.go new file mode 100644 index 000000000000..8b0a295f9713 --- /dev/null +++ b/op-service/jsonutil/write.go @@ -0,0 +1,18 @@ +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) +}