diff --git a/.circleci/config.yml b/.circleci/config.yml index 34131d7a1..470288f3d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,7 +24,7 @@ jobs: tag: '1.21' steps: - checkout - - run: golangci-lint run superchain/... validation/... + - run: golangci-lint run superchain/... validation/... add-chain/... golang-modules-tidy: executor: name: go/default # is based on cimg/go @@ -39,15 +39,36 @@ jobs: name: tidy validation module command: go mod tidy working_directory: validation + - run: + name: tidy add-chain module + command: go mod tidy + working_directory: add-chain - run: name: check git tree is clean command: git diff --exit-code golang-test: + shell: /bin/bash -eo pipefail executor: name: go/default # is based on cimg/go tag: '1.21' steps: - checkout + - run: + # need foundry to execute 'cast call' within add-chain script + name: Install foundry + command: | + echo "SHELL=$SHELL" + # Set up directory structure + mkdir -p $HOME/.foundry/bin + echo 'export PATH="$HOME/.foundry/bin:$PATH"' >> $BASH_ENV + source $BASH_ENV + + # Download foundryup and make it executable + curl -sSL "https://raw.githubusercontent.com/foundry-rs/foundry/master/foundryup/foundryup" -o $HOME/.foundry/bin/foundryup + chmod +x $HOME/.foundry/bin/foundryup + + $HOME/.foundry/bin/foundryup + forge --version - run: name: run superchain module tests command: go test ./... -v @@ -56,6 +77,10 @@ jobs: name: run validation module tests command: go test ./... -v working_directory: validation + - run: + name: run add-chain module tests + command: go test ./... -v + working_directory: add-chain - notify-failures-on-main publish-bot: environment: @@ -101,7 +126,7 @@ jobs: - run: name: check security configs command: sh ./scripts/check-security-configs.sh - - notify-failures-on-main + - notify-failures-on-main workflows: hourly: diff --git a/.gitignore b/.gitignore index 6d19b7664..d8e7ac195 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ docs/ .env .env* !.env.example +!.env.test *.log diff --git a/add-chain/.gitignore b/add-chain/.gitignore new file mode 100644 index 000000000..3980b7dc4 --- /dev/null +++ b/add-chain/.gitignore @@ -0,0 +1,2 @@ +# test output artifacts +testdata/**/awesomechain* diff --git a/add-chain/chain_config.go b/add-chain/chain_config.go new file mode 100644 index 000000000..8bf0a9647 --- /dev/null +++ b/add-chain/chain_config.go @@ -0,0 +1,121 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/ethereum-optimism/superchain-registry/superchain" + "gopkg.in/yaml.v3" +) + +// constructRollupConfig creates and populates a ChainConfig struct by reading from an input file and +// explicitly setting some additional fields to input argument values +func constructRollupConfig(inputFilePath, chainName, publicRPC, sequencerRPC, explorer string, superchainLevel superchain.SuperchainLevel) (superchain.ChainConfig, error) { + fmt.Printf("Attempting to read from %s\n", inputFilePath) + file, err := os.ReadFile(inputFilePath) + if err != nil { + return superchain.ChainConfig{}, fmt.Errorf("error reading file: %w", err) + } + var config superchain.ChainConfig + if err = json.Unmarshal(file, &config); err != nil { + return superchain.ChainConfig{}, fmt.Errorf("error unmarshaling json: %w", err) + } + + config.Name = chainName + config.PublicRPC = publicRPC + config.SequencerRPC = sequencerRPC + config.SuperchainLevel = superchainLevel + config.Explorer = explorer + + fmt.Printf("Rollup config successfully constructed\n") + return config, nil +} + +// writeChainConfig accepts a rollupConfig, formats it, and writes some output files based on the given +// target directories +func writeChainConfig( + rollupConfig superchain.ChainConfig, + targetDirectory string, + superchainRepoPath string, + superchainTarget string, +) error { + // Create genesis-system-config data + // (this is deprecated, users should load this from L1, when available via SystemConfig) + dirPath := filepath.Join(superchainRepoPath, "superchain", "extra", "genesis-system-configs", superchainTarget) + + if err := os.MkdirAll(dirPath, 0o755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + systemConfigJSON, err := json.MarshalIndent(rollupConfig.Genesis.SystemConfig, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal genesis system config json: %w", err) + } + + // Write the genesis system config JSON to a new file + filePath := filepath.Join(dirPath, rollupConfig.Name+".json") + if err := os.WriteFile(filePath, systemConfigJSON, 0o644); err != nil { + return fmt.Errorf("failed to write genesis system config json: %w", err) + } + fmt.Printf("Genesis system config written to: %s\n", filePath) + + rollupConfig.Genesis.SystemConfig = superchain.SystemConfig{} // remove SystemConfig so its omitted from yaml + + // Remove hardfork timestamp override fields if they match superchain defaults + defaults := superchain.Superchains[superchainTarget] + rollupConfig.SetDefaultHardforkTimestampsToNil(&defaults.Config) + + yamlData, err := yaml.Marshal(rollupConfig) + if err != nil { + return fmt.Errorf("failed to marshal yaml: %w", err) + } + + // Unmarshal bytes into a yaml.Node for custom manipulation + var rootNode yaml.Node + if err = yaml.Unmarshal(yamlData, &rootNode); err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err = rollupConfig.EnhanceYAML(ctx, &rootNode); err != nil { + return err + } + + // Write the rollup config to a yaml file + filename := filepath.Join(targetDirectory) + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + + encoder := yaml.NewEncoder(file) + defer encoder.Close() + + encoder.SetIndent(2) + if err := encoder.Encode(&rootNode); err != nil { + return fmt.Errorf("failed to write yaml file: %w", err) + } + + fmt.Printf("Rollup config written to: %s\n", filename) + return nil +} + +func getL1RpcUrl(superchainTarget string) (string, error) { + superChain, ok := superchain.Superchains[superchainTarget] + if !ok { + return "", fmt.Errorf("unknown superchain target provided: %s", superchainTarget) + } + + if superChain.Config.L1.PublicRPC == "" { + return "", fmt.Errorf("missing L1 public rpc endpoint in superchain config") + } + + fmt.Printf("Setting L1 public rpc endpoint to %s\n", superChain.Config.L1.PublicRPC) + return superChain.Config.L1.PublicRPC, nil +} diff --git a/add-chain/contract_addresses.go b/add-chain/contract_addresses.go new file mode 100644 index 000000000..20ac9d8b5 --- /dev/null +++ b/add-chain/contract_addresses.go @@ -0,0 +1,160 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/ethereum-optimism/superchain-registry/superchain" +) + +type AddressData struct { + Address string `json:"address"` +} + +var ( + // Addresses to retrieve from JSON + AddressManager = "AddressManager" + L1CrossDomainMessengerProxy = "L1CrossDomainMessengerProxy" + L1ERC721BridgeProxy = "L1ERC721BridgeProxy" + L1StandardBridgeProxy = "L1StandardBridgeProxy" + L2OutputOracleProxy = "L2OutputOracleProxy" + OptimismMintableERC20FactoryProxy = "OptimismMintableERC20FactoryProxy" + SystemConfigProxy = "SystemConfigProxy" + OptimismPortalProxy = "OptimismPortalProxy" + ProxyAdmin = "ProxyAdmin" + + // Addresses to retrieve from chain + SuperchainConfig = "SuperchainConfig" + Guardian = "Guardian" + Challenger = "Challenger" + ProxyAdminOwner = "ProxyAdminOwner" + SystemConfigOwner = "SystemConfigOwner" +) + +func readAddressesFromChain(contractAddresses map[string]string, l1RpcUrl string) error { + // SuperchainConfig + address, err := castCall(contractAddresses[OptimismPortalProxy], "superchainConfig()(address)", l1RpcUrl) + if err != nil { + contractAddresses[SuperchainConfig] = "" + } else { + contractAddresses[SuperchainConfig] = address + } + + // Guardian + address, err = castCall(contractAddresses[SuperchainConfig], "guardian()(address)", l1RpcUrl) + if err != nil { + address, err = castCall(contractAddresses[OptimismPortalProxy], "guardian()(address)", l1RpcUrl) + if err != nil { + return fmt.Errorf("could not retrieve address for Guardian %w", err) + } + } + contractAddresses[Guardian] = address + + // Challenger + address, err = castCall(contractAddresses[L2OutputOracleProxy], "challenger()(address)", l1RpcUrl) + if err != nil { + return fmt.Errorf("could not retrieve address for Guardian") + } + contractAddresses[Challenger] = address + + // ProxyAdminOwner + address, err = castCall(contractAddresses[ProxyAdmin], "owner()(address)", l1RpcUrl) + if err != nil { + return fmt.Errorf("could not retrieve address for ProxyAdminOwner") + } + contractAddresses[ProxyAdminOwner] = address + + // SystemConfigOwner + address, err = castCall(contractAddresses[SystemConfigProxy], "owner()(address)", l1RpcUrl) + if err != nil { + return fmt.Errorf("could not retrieve address for ProxyAdminOwner") + } + contractAddresses[SystemConfigOwner] = address + + fmt.Printf("Contract addresses read from on-chain contracts\n") + return nil +} + +func readAddressesFromJSON(contractAddresses map[string]string, deploymentsDir string) error { + contractsFromJSON := []string{ + AddressManager, + L1CrossDomainMessengerProxy, + L1ERC721BridgeProxy, + L1StandardBridgeProxy, + L2OutputOracleProxy, + OptimismMintableERC20FactoryProxy, + SystemConfigProxy, + OptimismPortalProxy, + ProxyAdmin, + } + + deployFilePath := filepath.Join(deploymentsDir, ".deploy") + _, err := os.Stat(deployFilePath) + + if err != nil { + // Use legacy deployment artifact schema + for _, name := range contractsFromJSON { + path := filepath.Join(deploymentsDir, name+".json") + file, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + var data AddressData + if err = json.Unmarshal(file, &data); err != nil { + return fmt.Errorf("failed to unmarshal json: %w", err) + } + contractAddresses[name] = data.Address + } + } else { + var addressList superchain.AddressList + rawData, err := os.ReadFile(deployFilePath) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + + if err = json.Unmarshal(rawData, &addressList); err != nil { + return fmt.Errorf("failed to unmarshal json: %w", err) + } + + for _, name := range contractsFromJSON { + address, err := addressList.AddressFor(name) + if err != nil { + return fmt.Errorf("failed to retrieve %s address from list: %w", name, err) + } + contractAddresses[name] = address.String() + } + } + + fmt.Printf("Contract addresses read from deployments directory: %s\n", deploymentsDir) + return nil +} + +func writeAddressesToJSON(contractsAddresses map[string]string, superchainRepoPath, target, chainName string) error { + dirPath := filepath.Join(superchainRepoPath, "superchain", "extra", "addresses", target) + if err := os.MkdirAll(dirPath, 0o755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + filePath := filepath.Join(dirPath, chainName+".json") + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer file.Close() + + // Marshal the map to JSON + jsonData, err := json.MarshalIndent(contractsAddresses, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal json: %w", err) + } + + // Write the JSON data to the file + if _, err := file.Write(jsonData); err != nil { + return fmt.Errorf("failed to write json to file: %w", err) + } + fmt.Printf("Contract addresses written to: %s\n", filePath) + + return nil +} diff --git a/add-chain/e2e_test.go b/add-chain/e2e_test.go new file mode 100644 index 000000000..5285a945a --- /dev/null +++ b/add-chain/e2e_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "bytes" + "encoding/json" + "os" + "reflect" + "testing" + + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v2" + "gopkg.in/yaml.v3" +) + +func TestCLIApp(t *testing.T) { + app := &cli.App{ + Name: "add-chain", + Usage: "Add a new chain to the superchain-registry", + Flags: []cli.Flag{ChainTypeFlag, TestFlag}, + Action: entrypoint, + } + + args := []string{"add-chain", "-chain-type", "standard", "-test", "true"} + err := app.Run(args) + require.NoError(t, err, "add-chain app failed") + + checkConfigYaml(t) + compareJsonFiles(t, "./testdata/superchain/extra/addresses/sepolia/") + compareJsonFiles(t, "./testdata/superchain/extra/genesis-system-configs/sepolia/") +} + +func compareJsonFiles(t *testing.T, dirPath string) { + expectedBytes, err := os.ReadFile(dirPath + "expected.json") + require.NoError(t, err, "failed to read expected.json file from "+dirPath) + + var expectJSON map[string]interface{} + err = json.Unmarshal(expectedBytes, &expectJSON) + require.NoError(t, err, "failed to unmarshal expected.json file from "+dirPath) + + testBytes, err := os.ReadFile(dirPath + "awesomechain.json") + require.NoError(t, err, "failed to read awesomechain.json file from "+dirPath) + + var testJSON map[string]interface{} + err = json.Unmarshal(testBytes, &testJSON) + require.NoError(t, err, "failed to read awesomechain.json file from "+dirPath) + + require.True(t, reflect.DeepEqual(expectJSON, testJSON), "awesomechain.json contents do not meet expectation:\n %s", string(testBytes)) +} + +func checkConfigYaml(t *testing.T) { + expectedBytes, err := os.ReadFile("./testdata/superchain/configs/sepolia/expected.yaml") + require.NoError(t, err, "failed to read expected.yaml config file: %w", err) + + var expectedYaml map[string]interface{} + err = yaml.Unmarshal(expectedBytes, &expectedYaml) + require.NoError(t, err, "failed to unmarshal expected.yaml config file: %w", err) + + testBytes, err := os.ReadFile("./testdata/superchain/configs/sepolia/awesomechain.yaml") + require.NoError(t, err, "failed to read awesomechain.yaml config file: %w", err) + + require.True(t, bytes.Equal(expectedBytes, testBytes), "awesomechain.yaml contents do not meet expectation:\n %s", string(testBytes)) +} diff --git a/add-chain/go.mod b/add-chain/go.mod new file mode 100644 index 000000000..1e37c1ec8 --- /dev/null +++ b/add-chain/go.mod @@ -0,0 +1,39 @@ +module github.com/ethereum-optimism/superchain-registry/add-chain + +go 1.21 + +require ( + github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240430193954-bd84389308cb + github.com/spf13/viper v1.18.2 + github.com/stretchr/testify v1.9.0 + github.com/urfave/cli/v2 v2.27.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect +) diff --git a/add-chain/go.sum b/add-chain/go.sum new file mode 100644 index 000000000..951d7d749 --- /dev/null +++ b/add-chain/go.sum @@ -0,0 +1,84 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240430193954-bd84389308cb h1:XdtWO/uWaUfAJxfBMqVjcWr0Irpzx0K+xastMzJhAmM= +github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240430193954-bd84389308cb/go.mod h1:7xh2awFQqsiZxFrHKTgEd+InVfDRrkKVUIuK8SAFHp0= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/add-chain/main.go b/add-chain/main.go new file mode 100644 index 000000000..f825a8ce1 --- /dev/null +++ b/add-chain/main.go @@ -0,0 +1,134 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/viper" + "github.com/urfave/cli/v2" +) + +var ( + ChainTypeFlag = &cli.StringFlag{ + Name: "chain-type", + Value: "", + Usage: "Type of chain (either standard or frontier)", + Required: true, + } + TestFlag = &cli.BoolFlag{ + Name: "test", + Value: false, + Usage: "Indicates if go tests are being run", + Required: false, + } +) + +func main() { + app := &cli.App{ + Name: "add-chain", + Usage: "Add a new chain to the superchain-registry", + Flags: []cli.Flag{ChainTypeFlag, TestFlag}, + Action: entrypoint, + } + + if err := app.Run(os.Args); err != nil { + fmt.Println(err) + fmt.Println("*********************") + fmt.Printf("FAILED: %s\n", app.Name) + os.Exit(1) + } + fmt.Println("*********************") + fmt.Printf("SUCCESS: %s\n", app.Name) +} + +func entrypoint(ctx *cli.Context) error { + chainType := ctx.String(ChainTypeFlag.Name) + runningTests := ctx.Bool(TestFlag.Name) + + superchainLevel, err := getSuperchainLevel(chainType) + if err != nil { + return fmt.Errorf("failed to get superchain level: %w", err) + } + + // Get the current script's directory + superchainRepoPath, err := os.Getwd() + envFilename := ".env" + envPath := "." + if err != nil { + return fmt.Errorf("error getting current directory: %w", err) + } + if runningTests { + envFilename = ".env.test" + envPath = "./testdata" + superchainRepoPath = filepath.Join(superchainRepoPath, "testdata") + } + + // Load environment variables + viper.SetConfigName(envFilename) // name of config file (without extension) + viper.SetConfigType("env") // REQUIRED if the config file does not have the extension in the name + viper.AddConfigPath(envPath) // path to look for the config file in + if err := viper.ReadInConfig(); err != nil { + return fmt.Errorf("error reading config file: %w", err) + } + + chainName := viper.GetString("CHAIN_NAME") + superchainTarget := viper.GetString("SUPERCHAIN_TARGET") + monorepoDir := viper.GetString("MONOREPO_DIR") + deploymentsDir := viper.GetString("DEPLOYMENTS_DIR") + rollupConfigPath := viper.GetString("ROLLUP_CONFIG") + publicRPC := viper.GetString("PUBLIC_RPC") + sequencerRPC := viper.GetString("SEQUENCER_RPC") + explorer := viper.GetString("EXPLORER") + + fmt.Printf("Chain Name: %s\n", chainName) + fmt.Printf("Superchain target: %s\n", superchainTarget) + fmt.Printf("Superchain-registry repo dir: %s\n", superchainRepoPath) + fmt.Printf("Reading from monrepo directory: %s\n", monorepoDir) + fmt.Printf("With deployments directory: %s\n", deploymentsDir) + fmt.Printf("Rollup config filepath: %s\n", rollupConfigPath) + fmt.Printf("Public RPC endpoint: %s\n", publicRPC) + fmt.Printf("Sequencer RPC endpoint: %s\n", sequencerRPC) + fmt.Printf("Block Explorer: %s\n", explorer) + fmt.Println() + + // Check if superchain target directory exists + targetDir := filepath.Join(superchainRepoPath, "superchain", "configs", superchainTarget) + if _, err := os.Stat(targetDir); os.IsNotExist(err) { + return fmt.Errorf("superchain target directory not found. Please follow instructions to add a superchain target in CONTRIBUTING.md") + } + + rollupConfig, err := constructRollupConfig(rollupConfigPath, chainName, publicRPC, sequencerRPC, explorer, superchainLevel) + if err != nil { + return fmt.Errorf("failed to construct rollup config: %w", err) + } + + targetFilePath := filepath.Join(targetDir, chainName+".yaml") + err = writeChainConfig(rollupConfig, targetFilePath, superchainRepoPath, superchainTarget) + if err != nil { + return fmt.Errorf("error generating chain config .yaml file: %w", err) + } + + contractAddresses := make(map[string]string) + err = readAddressesFromJSON(contractAddresses, deploymentsDir) + if err != nil { + return fmt.Errorf("failed to read addresses from JSON files: %w", err) + } + + l1RpcUrl, err := getL1RpcUrl(superchainTarget) + if err != nil { + return fmt.Errorf("failed to retrieve L1 rpc url: %w", err) + } + + err = readAddressesFromChain(contractAddresses, l1RpcUrl) + if err != nil { + return fmt.Errorf("failed to read addresses from chain: %w", err) + } + + err = writeAddressesToJSON(contractAddresses, superchainRepoPath, superchainTarget, chainName) + if err != nil { + return fmt.Errorf("failed to write contract addresses to JSON file: %w", err) + } + + return nil +} diff --git a/add-chain/testdata/.env.test b/add-chain/testdata/.env.test new file mode 100644 index 000000000..7960ee6fc --- /dev/null +++ b/add-chain/testdata/.env.test @@ -0,0 +1,9 @@ +# Required for both Standard Chains and Frontier Chains +CHAIN_NAME=awesomechain # L2 chain name +SUPERCHAIN_TARGET=sepolia # L1 chain name +MONOREPO_DIR=./testdata/monorepo +DEPLOYMENTS_DIR=${MONOREPO_DIR}/deployments +ROLLUP_CONFIG=${MONOREPO_DIR}/op-node/rollup.json +PUBLIC_RPC="http://awe.some.rpc" # L2 RPC URL +SEQUENCER_RPC="http://awe.some.seq.rpc" +EXPLORER="https://awesomescan.org" # L2 block explorer URL diff --git a/add-chain/testdata/monorepo/deployments/.deploy b/add-chain/testdata/monorepo/deployments/.deploy new file mode 100644 index 000000000..40cf7b257 --- /dev/null +++ b/add-chain/testdata/monorepo/deployments/.deploy @@ -0,0 +1,11 @@ +{ + "AddressManager": "0xBBCfC68f590ae60Feb1551F4C639E3Eab5ea7BA9", + "L1CrossDomainMessengerProxy": "0x6b95418119Aa1bc41D521377D98E6c367817A755", + "L1ERC721BridgeProxy": "0x460Cb843a61CeB6cFB3BC3373FC405744f5be4ee", + "L1StandardBridgeProxy": "0xC4Ed1Ae4671B38A44B3cea27E1F7462cFe6B1A87", + "L2OutputOracleProxy": "0x3F8A862E63E759a77DA22d384027D21BF096bA9E", + "OptimismMintableERC20FactoryProxy": "0x39f10A94B969587A2f1739b4AbE3b223814D2A07", + "SystemConfigProxy": "0x8787D3382F60d4e072b4276BBda065959195dB33", + "OptimismPortalProxy": "0x6C84AE91901CE8a4897187c33A0094a6e790f34d", + "ProxyAdmin": "0xd3fE61dd95c1e148A6ef540e1D4983d62726661C" +} diff --git a/add-chain/testdata/monorepo/op-node/rollup.json b/add-chain/testdata/monorepo/op-node/rollup.json new file mode 100644 index 000000000..a3053d26d --- /dev/null +++ b/add-chain/testdata/monorepo/op-node/rollup.json @@ -0,0 +1,34 @@ +{ + "genesis": { + "l1": { + "hash": "0x1fc104fe244042459314bc1d51a230320c64a3d47cd06b7998aa3f57b9b0fc79", + "number": 5753349 + }, + "l2": { + "hash": "0x0ceefff2113580e4d665c324661e4982a824025bf97285cf09a52715b6f89083", + "number": 0 + }, + "l2_time": 1713792864, + "system_config": { + "batcherAddr": "0x27bf581015c64ec7c57c6e7b3117f35b5f6dd706", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000834", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 30000000, + "baseFeeScalar": 0, + "blobBaseFeeScalar": 0 + } + }, + "block_time": 2, + "max_sequencer_drift": 600, + "seq_window_size": 3600, + "channel_timeout": 300, + "l1_chain_id": 11155111, + "l2_chain_id": 42069, + "regolith_time": 0, + "canyon_time": 0, + "delta_time": 1703203200, + "batch_inbox_address": "0xff00000000000000000000000000000000042069", + "deposit_contract_address": "0x6c84ae91901ce8a4897187c33a0094a6e790f34d", + "l1_system_config_address": "0x8787d3382f60d4e072b4276bbda065959195db33", + "protocol_versions_address": "0x0000000000000000000000000000000000000000" +} diff --git a/add-chain/testdata/superchain/configs/sepolia/expected.yaml b/add-chain/testdata/superchain/configs/sepolia/expected.yaml new file mode 100644 index 000000000..b03c2c132 --- /dev/null +++ b/add-chain/testdata/superchain/configs/sepolia/expected.yaml @@ -0,0 +1,20 @@ +name: awesomechain +chain_id: 42069 +public_rpc: http://awe.some.rpc +sequencer_rpc: http://awe.some.seq.rpc +explorer: https://awesomescan.org + +superchain_level: 2 + +batch_inbox_addr: 0xff00000000000000000000000000000000042069 + +genesis: + l1: + hash: 0x1fc104fe244042459314bc1d51a230320c64a3d47cd06b7998aa3f57b9b0fc79 + number: 5753349 + l2: + hash: 0x0ceefff2113580e4d665c324661e4982a824025bf97285cf09a52715b6f89083 + number: 0 + l2_time: 1713792864 # Mon 22 Apr 2024 13:34:24 UTC + +canyon_time: 0 # Thu 1 Jan 1970 00:00:00 UTC diff --git a/add-chain/testdata/superchain/extra/addresses/sepolia/expected.json b/add-chain/testdata/superchain/extra/addresses/sepolia/expected.json new file mode 100644 index 000000000..fd6de1fe6 --- /dev/null +++ b/add-chain/testdata/superchain/extra/addresses/sepolia/expected.json @@ -0,0 +1,16 @@ +{ + "AddressManager": "0xbbcfc68f590ae60feb1551f4c639e3eab5ea7ba9", + "Challenger": "0xe8D525b30dD4C406570d47396b5b237DB30F13F1", + "Guardian": "0xe8D525b30dD4C406570d47396b5b237DB30F13F1", + "L1CrossDomainMessengerProxy": "0x6b95418119aa1bc41d521377d98e6c367817a755", + "L1ERC721BridgeProxy": "0x460cb843a61ceb6cfb3bc3373fc405744f5be4ee", + "L1StandardBridgeProxy": "0xc4ed1ae4671b38a44b3cea27e1f7462cfe6b1a87", + "L2OutputOracleProxy": "0x3f8a862e63e759a77da22d384027d21bf096ba9e", + "OptimismMintableERC20FactoryProxy": "0x39f10a94b969587a2f1739b4abe3b223814d2a07", + "OptimismPortalProxy": "0x6c84ae91901ce8a4897187c33a0094a6e790f34d", + "ProxyAdmin": "0xd3fe61dd95c1e148a6ef540e1d4983d62726661c", + "ProxyAdminOwner": "0x9663FF52F1666954551460FcF190b4E3DaECab37", + "SuperchainConfig": "0xdf69B06cEB4A4b4Db3CC288840435e673ac4d893", + "SystemConfigOwner": "0xe8D525b30dD4C406570d47396b5b237DB30F13F1", + "SystemConfigProxy": "0x8787d3382f60d4e072b4276bbda065959195db33" +} diff --git a/add-chain/testdata/superchain/extra/genesis-system-configs/sepolia/expected.json b/add-chain/testdata/superchain/extra/genesis-system-configs/sepolia/expected.json new file mode 100644 index 000000000..6dba71a85 --- /dev/null +++ b/add-chain/testdata/superchain/extra/genesis-system-configs/sepolia/expected.json @@ -0,0 +1,8 @@ +{ + "batcherAddr": "0x27bf581015c64ec7c57c6e7b3117f35b5f6dd706", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000834", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 30000000, + "baseFeeScalar": 0, + "blobBaseFeeScalar": 0 +} diff --git a/add-chain/utils.go b/add-chain/utils.go new file mode 100644 index 000000000..4aa8a64df --- /dev/null +++ b/add-chain/utils.go @@ -0,0 +1,41 @@ +package main + +import ( + "bytes" + "fmt" + "os/exec" + "strings" + + "github.com/ethereum-optimism/superchain-registry/superchain" +) + +func castCall(contractAddress, calldata, l1RpcUrl string) (string, error) { + cmd := exec.Command("cast", "call", contractAddress, calldata, "-r", l1RpcUrl) + var out bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("%s, %w", &stderr, err) + } + + address := strings.Join(strings.Fields(out.String()), "") // remove whitespace + if address == "" || address == "0x" { + return "", fmt.Errorf("cast call returned empty address") + } + + return address, nil +} + +func getSuperchainLevel(chainType string) (superchain.SuperchainLevel, error) { + switch chainType { + case "standard": + fmt.Printf("Adding standard chain to superchain-registry...\n\n") + return superchain.Standard, nil + case "frontier": + fmt.Printf("Adding frontier chain to superchain-registry...\n\n") + return superchain.Frontier, nil + } + + return 0, fmt.Errorf("invalid chain type: %s", chainType) +} diff --git a/go.work b/go.work index 49279df3d..b7935e534 100644 --- a/go.work +++ b/go.work @@ -1,6 +1,7 @@ -go 1.21 +go 1.21.9 use ( - ./superchain - ./validation + ./add-chain + ./superchain + ./validation ) diff --git a/scripts/add-chain.sh b/scripts/add-chain.sh index 7b342df9f..f88555532 100644 --- a/scripts/add-chain.sh +++ b/scripts/add-chain.sh @@ -1,161 +1,15 @@ set -e -show_usage() { - echo "Usage: $0 [-h|--help]" - echo " chain_type: The type of chain to add. Must be 'standard' or 'frontier'." - echo " -h, --help: Show this usage information." -} - -if [[ $# -eq 0 || $1 == "-h" || $1 == "--help" ]]; then - show_usage - exit 0 -fi - -TYPE=$1 - -case $TYPE in - "standard") - echo "Adding $TYPE chain to superchain-registry..." - SUPERCHAIN_LEVEL=2 - ;; - "frontier") - echo "Adding $TYPE chain to superchain-registry..." - SUPERCHAIN_LEVEL=1 - ;; - *) - echo "Invalid chain type $TYPE" - show_usage - exit 1 - ;; -esac +go run ./add-chain -chain-type=$1 # Get the absolute path of the current script, including following symlinks SCRIPT_PATH=$(readlink -f "$0" || realpath "$0") # Get the directory of the current script SCRIPT_DIR=$(dirname "$SCRIPT_PATH") # Get the parent directory of the script's directory -PARENT_DIR=$(dirname "$SCRIPT_DIR") - -SUPERCHAIN_REPO=${PARENT_DIR} - - -# load and echo env vars +SUPERCHAIN_REPO=$(dirname "$SCRIPT_DIR") source ${SUPERCHAIN_REPO}/.env -echo "Chain Name ${CHAIN_NAME}" -echo "Superchain target: ${SUPERCHAIN_TARGET}" -echo "Reading from monrepo directory: ${MONOREPO_DIR}" -echo "With deployments directory: ${DEPLOYMENTS_DIR}" -echo "Rollup config: ${ROLLUP_CONFIG}" -echo "Genesis config: ${GENESIS_CONFIG}" -echo "Public RPC endpoint: ${PUBLIC_RPC}" -echo "Sequencer RPC endpoint: ${SEQUENCER_RPC}" -echo "Block Explorer: ${EXPLORER}" - -[ -d "$SUPERCHAIN_REPO/superchain/configs/$SUPERCHAIN_TARGET" ] || { echo "Superchain target directory not found. Please follow instructions to "adding a superchain target" in CONTRIBUTING.md"; exit 1; } - - -# add chain config -cat > $SUPERCHAIN_REPO/superchain/configs/$SUPERCHAIN_TARGET/$CHAIN_NAME.yaml << EOF -name: $CHAIN_NAME -chain_id: $(jq -j .l2_chain_id $ROLLUP_CONFIG) -public_rpc: $PUBLIC_RPC -sequencer_rpc: $SEQUENCER_RPC -explorer: $EXPLORER - -superchain_level: $SUPERCHAIN_LEVEL - -batch_inbox_addr: "$(jq -j .batch_inbox_address $ROLLUP_CONFIG)" - -genesis: - l1: - hash: "$(jq -j .genesis.l1.hash $ROLLUP_CONFIG)" - number: $(jq -j .genesis.l1.number $ROLLUP_CONFIG) - l2: - hash: "$(jq -j .genesis.l2.hash $ROLLUP_CONFIG)" - number: $(jq -j .genesis.l2.number $ROLLUP_CONFIG) - l2_time: $(jq -j .genesis.l2_time $ROLLUP_CONFIG) - -canyon_time: $(jq -j .canyon_time $ROLLUP_CONFIG) -delta_time: $(jq -j .delta_time $ROLLUP_CONFIG) -ecotone_time: $(jq -j .ecotone_time $ROLLUP_CONFIG) - -EOF - - -# infer appropriate L1_RPC_URL -case $SUPERCHAIN_TARGET in - "mainnet") - L1_RPC_URL="https://ethereum-mainnet-rpc.allthatnode.com" - ;; - "sepolia") - L1_RPC_URL="https://ethereum-sepolia-rpc.allthatnode.com" - ;; - *) - echo "Unsupported Superchain Target $SUPERCHAIN_TARGET" - exit 1 - ;; -esac - -# scrape addresses from static deployment artifact -if [ -e $DEPLOYMENTS_DIR/.deploy ] -then - AddressManager=$(jq -j .AddressManager $DEPLOYMENTS_DIR/.deploy) - L1CrossDomainMessengerProxy=$(jq -j .L1CrossDomainMessengerProxy $DEPLOYMENTS_DIR/.deploy) - L1ERC721BridgeProxy=$(jq -j .L1ERC721BridgeProxy $DEPLOYMENTS_DIR/.deploy) - L1StandardBridgeProxy=$(jq -j .L1StandardBridgeProxy $DEPLOYMENTS_DIR/.deploy) - L2OutputOracleProxy=$(jq -j .L2OutputOracleProxy $DEPLOYMENTS_DIR/.deploy) - OptimismMintableERC20FactoryProxy=$(jq -j .OptimismMintableERC20FactoryProxy $DEPLOYMENTS_DIR/.deploy) - SystemConfigProxy=$(jq -j .SystemConfigProxy $DEPLOYMENTS_DIR/.deploy) - OptimismPortalProxy=$(jq -j .OptimismPortalProxy $DEPLOYMENTS_DIR/.deploy) - SystemConfigProxy=$(jq -j .SystemConfigProxy $DEPLOYMENTS_DIR/.deploy) -else # use legacy deployment artifact schema - AddressManager=$(jq -j .address $DEPLOYMENTS_DIR/AddressManager.json) - L1CrossDomainMessengerProxy=$(jq -j .address $DEPLOYMENTS_DIR/L1CrossDomainMessengerProxy.json) - L1ERC721BridgeProxy=$(jq -j .address $DEPLOYMENTS_DIR/L1ERC721BridgeProxy.json) - L1StandardBridgeProxy=$(jq -j .address $DEPLOYMENTS_DIR/L1StandardBridgeProxy.json) - L2OutputOracleProxy=$(jq -j .address $DEPLOYMENTS_DIR/L2OutputOracleProxy.json) - OptimismMintableERC20FactoryProxy=$(jq -j .address $DEPLOYMENTS_DIR/OptimismMintableERC20FactoryProxy.json) - SystemConfigProxy=$(jq -j .address $DEPLOYMENTS_DIR/SystemConfigProxy.json) - OptimismPortalProxy=$(jq -j .address $DEPLOYMENTS_DIR/OptimismPortalProxy.json) - SystemConfigProxy=$(jq -j .address $DEPLOYMENTS_DIR/SystemConfigProxy.json) -fi - -# scrape remaining address live from the chain -SuperchainConfig=$(cast call $OptimismPortalProxy "superchainConfig()(address)" -r $L1_RPC_URL) || "" # first command could fail if bedrock -Guardian=$(cast call $SuperchainConfig "guardian()(address)" -r $L1_RPC_URL) || $(cast call $OptimismPortalProxy "GUARDIAN()(address)" -r $L1_RPC_URL) # first command could fail if bedrock -Challenger=$(cast call $L2OutputOracleProxy "challenger()(address)" -r $L1_RPC_URL) # first command could fail if FPAC: TODO figure out how to get this role if FPAC -ProxyAdmin=$(jq -j .address $DEPLOYMENTS_DIR/ProxyAdmin.json) -ProxyAdminOwner=$(cast call $ProxyAdmin "owner()(address)" -r $L1_RPC_URL) -SystemConfigOwner=$(cast call $SystemConfigProxy "owner()(address)" -r $L1_RPC_URL) - -# add extra addresses data to registry -mkdir -p $SUPERCHAIN_REPO/superchain/extra/addresses/$SUPERCHAIN_TARGET -cat > $SUPERCHAIN_REPO/superchain/extra/addresses/$SUPERCHAIN_TARGET/$CHAIN_NAME.json << EOF -{ - "AddressManager": "$AddressManager", - "L1CrossDomainMessengerProxy": "$L1CrossDomainMessengerProxy", - "L1ERC721BridgeProxy": "$L1ERC721BridgeProxy", - "L1StandardBridgeProxy": "$L1StandardBridgeProxy", - "L2OutputOracleProxy": "$L2OutputOracleProxy", - "OptimismMintableERC20FactoryProxy": "$OptimismMintableERC20FactoryProxy", - "OptimismPortalProxy": "$OptimismPortalProxy", - "SystemConfigProxy": "$SystemConfigProxy", - "ProxyAdmin": "$ProxyAdmin", - "ProxyAdminOwner": "$ProxyAdminOwner", - "SystemConfigOwner": "$SystemConfigOwner", - "Guardian": "$Guardian", - "Challenger": "$Challenger" -} -EOF - - -# create genesis-system-config data -# (this is deprecated, users should load this from L1, when available via SystemConfig). -mkdir -p $SUPERCHAIN_REPO/superchain/extra/genesis-system-configs/$SUPERCHAIN_TARGET -jq -r .genesis.system_config $ROLLUP_CONFIG > $SUPERCHAIN_REPO/superchain/extra/genesis-system-configs/$SUPERCHAIN_TARGET/$CHAIN_NAME.json - - # create extra genesis data mkdir -p $SUPERCHAIN_REPO/superchain/extra/genesis/$SUPERCHAIN_TARGET cd $MONOREPO_DIR diff --git a/superchain/superchain.go b/superchain/superchain.go index e5d8e09d1..5054ff7c1 100644 --- a/superchain/superchain.go +++ b/superchain/superchain.go @@ -2,6 +2,7 @@ package superchain import ( "compress/gzip" + "context" "embed" "encoding/json" "errors" @@ -10,6 +11,7 @@ import ( "io/fs" "path" "reflect" + "strconv" "strings" "time" @@ -36,17 +38,38 @@ type BlockID struct { } type ChainGenesis struct { - L1 BlockID `yaml:"l1"` - L2 BlockID `yaml:"l2"` - L2Time uint64 `yaml:"l2_time"` - ExtraData *HexBytes `yaml:"extra_data,omitempty"` + L1 BlockID `yaml:"l1"` + L2 BlockID `yaml:"l2"` + L2Time uint64 `json:"l2_time" yaml:"l2_time"` + ExtraData *HexBytes `yaml:"extra_data,omitempty"` + SystemConfig SystemConfig `json:"system_config" yaml:"system_config,omitempty"` +} + +type SystemConfig struct { + BatcherAddr string `json:"batcherAddr"` + Overhead string `json:"overhead"` + Scalar string `json:"scalar"` + GasLimit uint64 `json:"gasLimit"` + BaseFeeScalar uint64 `json:"baseFeeScalar"` + BlobBaseFeeScalar uint64 `json:"blobBaseFeeScalar"` +} + +type GenesisData struct { + L1 GenesisLayer `json:"l1" yaml:"l1"` + L2 GenesisLayer `json:"l2" yaml:"l2"` + L2Time int `json:"l2_time" yaml:"l2_time"` +} + +type GenesisLayer struct { + Hash string `json:"hash" yaml:"hash"` + Number int `json:"number" yaml:"number"` } type HardForkConfiguration struct { - CanyonTime *uint64 `yaml:"canyon_time,omitempty"` - DeltaTime *uint64 `yaml:"delta_time,omitempty"` - EcotoneTime *uint64 `yaml:"ecotone_time,omitempty"` - FjordTime *uint64 `yaml:"fjord_time,omitempty"` + CanyonTime *uint64 `json:"canyon_time,omitempty" yaml:"canyon_time,omitempty"` + DeltaTime *uint64 `json:"delta_time,omitempty" yaml:"delta_time,omitempty"` + EcotoneTime *uint64 `json:"ecotone_time,omitempty" yaml:"ecotone_time,omitempty"` + FjordTime *uint64 `json:"fjord_time,omitempty" yaml:"fjord_time,omitempty"` } type SuperchainLevel uint @@ -58,16 +81,16 @@ const ( type ChainConfig struct { Name string `yaml:"name"` - ChainID uint64 `yaml:"chain_id"` + ChainID uint64 `json:"l2_chain_id" yaml:"chain_id"` PublicRPC string `yaml:"public_rpc"` SequencerRPC string `yaml:"sequencer_rpc"` Explorer string `yaml:"explorer"` SuperchainLevel SuperchainLevel `yaml:"superchain_level"` - BatchInboxAddr Address `yaml:"batch_inbox_addr"` + BatchInboxAddr Address `json:"batch_inbox_address" yaml:"batch_inbox_addr"` - Genesis ChainGenesis `yaml:"genesis"` + Genesis ChainGenesis `json:"genesis" yaml:"genesis"` // Superchain is a simple string to identify the superchain. // This is implied by directory structure, and not encoded in the config file itself. @@ -80,6 +103,21 @@ type ChainConfig struct { HardForkConfiguration `yaml:",inline"` } +// SetDefaultHardforkTimestampsToNil sets each hardfork timestamp to nil (to remove the override) +// if the timestamp matches the superchain default +func (c *ChainConfig) SetDefaultHardforkTimestampsToNil(s *SuperchainConfig) { + cVal := reflect.ValueOf(&c.HardForkConfiguration).Elem() + sVal := reflect.ValueOf(&s.hardForkDefaults).Elem() + + for i := 0; i < reflect.Indirect(cVal).NumField(); i++ { + overrideValue := cVal.Field(i) + defaultValue := sVal.Field(i) + if reflect.DeepEqual(overrideValue.Interface(), defaultValue.Interface()) { + overrideValue.Set(reflect.Zero(overrideValue.Type())) + } + } +} + // setNilHardforkTimestampsToDefault overwrites each unspecified hardfork activation time override // with the superchain default. func (c *ChainConfig) setNilHardforkTimestampsToDefault(s *SuperchainConfig) { @@ -103,6 +141,55 @@ func (c *ChainConfig) setNilHardforkTimestampsToDefault(s *SuperchainConfig) { // ...etc for each field in HardForkConfiguration } +// EnhanceYAML creates a customized yaml string from a RollupConfig. After completion, +// the *yaml.Node pointer can be used with a yaml encoder to write the custom format to file +func (c *ChainConfig) EnhanceYAML(ctx context.Context, node *yaml.Node) error { + // Check if context is done before processing + if err := ctx.Err(); err != nil { + return fmt.Errorf("context error: %w", err) + } + + if node.Kind == yaml.DocumentNode && len(node.Content) > 0 { + node = node.Content[0] // Dive into the document node + } + + var lastKey string + for i := 0; i < len(node.Content)-1; i += 2 { + keyNode := node.Content[i] + valNode := node.Content[i+1] + + // Add blank line AFTER these keys + if lastKey == "explorer" || lastKey == "superchain_level" || lastKey == "genesis" { + keyNode.HeadComment = "\n" + } + + // Add blank line BEFORE these keys + if keyNode.Value == "genesis" { + keyNode.HeadComment = "\n" + } + + // Recursive call to check nested fields for "_time" suffix + if valNode.Kind == yaml.MappingNode { + if err := c.EnhanceYAML(ctx, valNode); err != nil { + return err + } + } + + // Add human readable timestamp in comment + if strings.HasSuffix(keyNode.Value, "_time") { + t, err := strconv.ParseInt(valNode.Value, 10, 64) + if err != nil { + return fmt.Errorf("failed to convert yaml string timestamp to int: %w", err) + } + timestamp := time.Unix(t, 0).UTC() + keyNode.LineComment = timestamp.Format("Mon 2 Jan 2006 15:04:05 UTC") + } + + lastKey = keyNode.Value + } + return nil +} + // AddressList represents the set of network specific contracts for a given network. type AddressList struct { AddressManager Address `json:"AddressManager"` @@ -121,6 +208,8 @@ type AddressList struct { func (a AddressList) AddressFor(contractName string) (Address, error) { var address Address switch contractName { + case "AddressManager": + address = a.AddressManager case "ProxyAdmin": address = a.ProxyAdmin case "L1CrossDomainMessengerProxy":