diff --git a/.circleci/config.yml b/.circleci/config.yml index 7dd095754..2bb8087c1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -60,6 +60,22 @@ commands: $HOME/.foundry/bin/foundryup forge --version + install-gvm: + description: "Installs gvm" + steps: + - run: + command: | + apt-get update + apt-get -yq install curl git mercurial make binutils bison gcc build-essential bsdmainutils + bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer) + source /root/.gvm/scripts/gvm + validate-genesis: + description: "Runs genesis validation checks" + steps: + - run: + name: Clone monorepo + command: mkdir ../optimism-temporary && git clone https://github.com/ethereum-optimism/optimism.git ../optimism-temporary + - run: just validate-genesis "" jobs: golang-lint: @@ -104,6 +120,16 @@ jobs: command: just test-add-chain - notify-failures-on-main: channel: C03N11M0BBN # to slack channel `notify-ci-failures` + golang-validate-genesis: + docker: + - image: us-docker.pkg.dev/oplabs-tools-artifacts/images/ci-builder:v0.49.0 + resource_class: medium + steps: + - checkout + # - install-just + - install-foundry + - install-gvm + - validate-genesis # TODO this should also be filtered on modified chains golang-validate-modified: shell: /bin/bash -eo pipefail executor: @@ -241,6 +267,7 @@ workflows: - golang-lint - golang-modules-tidy - golang-test + - golang-validate-genesis - golang-validate-modified - check-codegen - cargo-tests diff --git a/justfile b/justfile index fbddcf6a9..4172e3b72 100644 --- a/justfile +++ b/justfile @@ -48,6 +48,10 @@ validate-modified-chains REF: validate CHAIN_ID: TEST_DIRECTORY=./validation go run gotest.tools/gotestsum@latest --format testname -- -run='TestValidation/.+\({{CHAIN_ID}}\)$' -count=1 +# Run genesis validation (this is separated from other validation checks, because it is not a part of drift detection) +validate-genesis CHAIN_ID: + TEST_DIRECTORY=./validation/genesis go run gotest.tools/gotestsum@latest --format testname -- -run='TestGenesisPredeploys/.+\({{CHAIN_ID}}\)$' -v + promotion-test: TEST_DIRECTORY=./validation go run gotest.tools/gotestsum@latest --format dots -- -run Promotion diff --git a/superchain/superchain.go b/superchain/superchain.go index 4a5aade2b..893d52d47 100644 --- a/superchain/superchain.go +++ b/superchain/superchain.go @@ -119,29 +119,6 @@ func (c ChainConfig) Identifier() string { return c.Superchain + "/" + c.Chain } -// Returns a shallow copy of the chain config with some fields mutated -// to declare the chain a standard chain. No fields on the receiver -// are mutated. -func (c *ChainConfig) PromoteToStandard() (*ChainConfig, error) { - if !c.StandardChainCandidate { - return nil, errors.New("can only promote standard candidate chains") - } - if c.SuperchainLevel != Frontier { - return nil, errors.New("can only promote frontier chains") - } - - // Note that any pointers in c are copied to d - // This is not problematic as long as we do - // not modify the values pointed to. - d := *c - - d.StandardChainCandidate = false - d.SuperchainLevel = Standard - now := uint64(time.Now().Unix()) - d.SuperchainTime = &now - return &d, nil -} - type AltDAConfig struct { DAChallengeAddress *Address `json:"da_challenge_contract_address" toml:"da_challenge_contract_address"` // DA challenge window value set on the DAC contract. Used in altDA mode diff --git a/validation/genesis/foundry-config.patch b/validation/genesis/foundry-config.patch new file mode 100644 index 000000000..dcdf635b8 --- /dev/null +++ b/validation/genesis/foundry-config.patch @@ -0,0 +1,13 @@ +diff --git a/packages/contracts-bedrock/foundry.toml b/packages/contracts-bedrock/foundry.toml +index 3d11af94b..c3d441735 100644 +--- a/packages/contracts-bedrock/foundry.toml ++++ b/packages/contracts-bedrock/foundry.toml +@@ -8,7 +8,7 @@ remappings = [ + '@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/', + '@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/', + '@rari-capital/solmate/=node_modules/@rari-capital/solmate', +- "@cwia/=node_modules/clones-with-immutable-args/src", ++ "@cwia/=node_modules/clones-with-immutable-args", + 'forge-std/=node_modules/forge-std/src', + 'ds-test/=node_modules/ds-test/src' + ] diff --git a/validation/genesis/genesis-predeploy_test.go b/validation/genesis/genesis-predeploy_test.go new file mode 100644 index 000000000..938eb77f7 --- /dev/null +++ b/validation/genesis/genesis-predeploy_test.go @@ -0,0 +1,195 @@ +package genesis + +import ( + "encoding/json" + "fmt" + "log" + "os" + "os/exec" + "path" + "path/filepath" + "reflect" + "runtime" + "strconv" + "strings" + "testing" + + "github.com/ethereum-optimism/superchain-registry/superchain" + . "github.com/ethereum-optimism/superchain-registry/superchain" + "github.com/ethereum/go-ethereum/core" + "github.com/stretchr/testify/require" +) + +// TODO deduplicate this +// perChainTestName ensures test can easily be filtered by chain name or chain id using the -run=regex testflag. +func perChainTestName(chain *superchain.ChainConfig) string { + return chain.Name + fmt.Sprintf(" (%d)", chain.ChainID) +} + +func TestGenesisPredeploys(t *testing.T) { + for _, chain := range OPChains { + if chain.SuperchainLevel == Standard || chain.StandardChainCandidate { + t.Run(perChainTestName(chain), func(t *testing.T) { + // Do not run in parallel + testGenesisPredeploys(t, chain) + }) + } + } +} + +// Invoke this with go test -timeout 0 ./validation/genesis -run=TestGenesisPredeploys -v +// REQUIREMENTS: +// pnpm and yarn, so we can prepare https://codeload.github.com/Saw-mon-and-Natalie/clones-with-immutable-args/tar.gz/105efee1b9127ed7f6fedf139e1fc796ce8791f2 +func testGenesisPredeploys(t *testing.T, chain *ChainConfig) { + chainId := chain.ChainID + + vis, ok := ValidationInputs[chainId] + + if !ok { + t.Skip("WARNING: cannot yet validate this chain (no validation metadata)") + + } + + monorepoCommit := vis.GenesisCreationCommit + + // Setup some directory references + thisDir := getDirOfThisFile() + chainIdString := strconv.Itoa(int(chainId)) + validationInputsDir := path.Join(thisDir, "validation-inputs", chainIdString) + monorepoDir := path.Join(thisDir, "../../../optimism-temporary") + contractsDir := path.Join(monorepoDir, "packages/contracts-bedrock") + + // reset to appropriate commit, this is preferred to git checkout because it will + // blow away any leftover files from the previous run + executeCommandInDir(t, monorepoDir, exec.Command("git", "reset", "--hard", monorepoCommit)) + + // TODO unskip these, I am skipping to save time in development since we + // are not validating multiple chains yet + if false { + executeCommandInDir(t, monorepoDir, exec.Command("rm", "-rf", "node_modules")) + executeCommandInDir(t, contractsDir, exec.Command("rm", "-rf", "node_modules")) + } + + // install dependencies + // TODO we expect this step to vary as we scan through the monorepo history + // so we will need some branching logic here + // executeCommandInDir(t, contractsDir, exec.Command("pnpm", "install", "--no-frozen-lockfile")) + executeCommandInDir(t, contractsDir, exec.Command("yarn", "install", "--no-frozen-lockfile")) + + if monorepoCommit == "d80c145e0acf23a49c6a6588524f57e32e33b91" { + // apply a patch to get things working + // then compile the contracts + // TODO not sure why this is needed, it is likely coupled to the specific commit we are looking at + executeCommandInDir(t, thisDir, exec.Command("cp", "foundry-config.patch", contractsDir)) + executeCommandInDir(t, contractsDir, exec.Command("git", "apply", "foundry-config.patch")) + executeCommandInDir(t, contractsDir, exec.Command("forge", "build")) + // revert patch, makes rerunning script locally easier + executeCommandInDir(t, contractsDir, exec.Command("git", "apply", "-R", "foundry-config.patch")) + } + + // copy genesis input files to monorepo + executeCommandInDir(t, validationInputsDir, + exec.Command("cp", "deploy-config.json", path.Join(contractsDir, "deploy-config", chainIdString+".json"))) + err := os.MkdirAll(path.Join(contractsDir, "deployments", chainIdString), os.ModePerm) + if err != nil { + log.Fatalf("Failed to create directory: %v", err) + } + // err = writeDeployments(chainId, path.Join(contractsDir, "deployments", chainIdString)) + // if err != nil { + // log.Fatalf("Failed to write deployments: %v", err) + // } + writeDeploymentsLegacy(chainId, path.Join(contractsDir, "deployments", chainIdString)) + + // regenerate genesis.json at this monorepo commit. + executeCommandInDir(t, thisDir, exec.Command("cp", "./monorepo-outputs.sh", monorepoDir)) + executeCommandInDir(t, monorepoDir, exec.Command("sh", "./monorepo-outputs.sh", strings.Join(vis.GenesisCreationCommand, " "))) + + expectedData, err := os.ReadFile(path.Join(monorepoDir, "expected-genesis.json")) + require.NoError(t, err) + + gen := core.Genesis{} + + err = json.Unmarshal(expectedData, &gen) + require.NoError(t, err) + + expectedData, err = json.Marshal(gen.Alloc) + require.NoError(t, err) + + g, err := core.LoadOPStackGenesis(chainId) + require.NoError(t, err) + + gotData, err := json.Marshal(g.Alloc) + require.NoError(t, err) + + os.WriteFile(path.Join(monorepoDir, "want-alloc.json"), expectedData, 0777) + os.WriteFile(path.Join(monorepoDir, "got-alloc.json"), gotData, 0777) + + require.Equal(t, string(expectedData), string(gotData)) +} + +func getDirOfThisFile() string { + _, filename, _, ok := runtime.Caller(0) + if !ok { + panic("No caller information") + } + return filepath.Dir(filename) +} + +func writeDeployments(chainId uint64, directory string) error { + as := Addresses[chainId] + + data, err := json.Marshal(as) + if err != nil { + return err + } + + err = os.WriteFile(path.Join(directory, ".deploy"), data, 0777) + if err != nil { + return err + } + return nil +} + +func writeDeploymentsLegacy(chainId uint64, directory string) error { + + // Initialize your struct with some data + data := Addresses[chainId] + + // Get the reflection value object + val := reflect.ValueOf(*data) + typ := reflect.TypeOf(*data) + + // Iterate over the struct fields + for i := 0; i < val.NumField(); i++ { + fieldName := typ.Field(i).Name // Get the field name + fieldValue := val.Field(i).String() // Get the field value (assuming it's a string) + + // Define the JSON object + jsonData := map[string]string{ + "address": fieldValue, + } + + // Convert the map to JSON + fileContent, err := json.MarshalIndent(jsonData, "", " ") + if err != nil { + return fmt.Errorf("Failed to marshal JSON for field %s: %v", fieldName, err) + } + + // Create a file named after the field name + fileName := fmt.Sprintf("%s.json", fieldName) + file, err := os.Create(path.Join(directory, fileName)) + if err != nil { + return fmt.Errorf("Failed to create file for field %s: %v", fieldName, err) + } + defer file.Close() + + // Write the JSON content to the file + _, err = file.Write(fileContent) + if err != nil { + return fmt.Errorf("Failed to write JSON to file for field %s: %v", fieldName, err) + } + + fmt.Printf("Created file: %s\n", fileName) + } + return nil +} diff --git a/validation/genesis/genesis.go b/validation/genesis/genesis.go new file mode 100644 index 000000000..8f859b165 --- /dev/null +++ b/validation/genesis/genesis.go @@ -0,0 +1,56 @@ +package genesis + +import ( + "embed" + "fmt" + "path" + "strconv" + + "github.com/BurntSushi/toml" +) + +//go:embed validation-inputs +var validationInputs embed.FS + +var ValidationInputs map[uint64]ValidationMetadata + +func init() { + ValidationInputs = make(map[uint64]ValidationMetadata) + + chains, err := validationInputs.ReadDir("validation-inputs") + if err != nil { + panic(fmt.Errorf("failed to read validation-inputs dir: %w", err)) + } + // iterate over superchain-target entries + for _, s := range chains { + + if !s.IsDir() { + continue // ignore files, e.g. a readme + } + + // Load superchain-target config + metadata, err := validationInputs.ReadFile(path.Join("validation-inputs", s.Name(), "meta.toml")) + if err != nil { + panic(fmt.Errorf("failed to read metadata file: %w", err)) + } + + m := new(ValidationMetadata) + err = toml.Unmarshal(metadata, m) + if err != nil { + panic(fmt.Errorf("failed to decode metadata file: %w", err)) + } + + chainID, err := strconv.Atoi(s.Name()) + if err != nil { + panic(fmt.Errorf("failed to decode chain id from dir name: %w", err)) + } + + ValidationInputs[uint64(chainID)] = *m + + } +} + +type ValidationMetadata struct { + GenesisCreationCommit string `toml:"genesis_creation_commit"` // in https://github.com/ethereum-optimism/optimism/ + GenesisCreationCommand []string `toml:"genesis_creation_command"` +} diff --git a/validation/genesis/monorepo-outputs.sh b/validation/genesis/monorepo-outputs.sh new file mode 100755 index 000000000..0affb76ba --- /dev/null +++ b/validation/genesis/monorepo-outputs.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e + +go_version=$(grep -m 1 '^go ' go.mod | awk '{print $2}') + +# Source the gvm script to load gvm functions into the shell +set +e +source ~/.gvm/scripts/gvm || exit 1 +gvm install go${go_version} || exit 1 +gvm use go${go_version} || exit 1 +set -e + +echo "Running op-node genesis l2 command" + +eval "$1" diff --git a/validation/genesis/utils.go b/validation/genesis/utils.go new file mode 100644 index 000000000..add0de08a --- /dev/null +++ b/validation/genesis/utils.go @@ -0,0 +1,23 @@ +package genesis + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "testing" +) + +func executeCommandInDir(t *testing.T, dir string, cmd *exec.Cmd) { + t.Logf("executing %s", cmd.String()) + cmd.Dir = dir + var outErr bytes.Buffer + cmd.Stdout = os.Stdout + cmd.Stderr = &outErr + err := cmd.Run() + if err != nil { + // error case : status code of command is different from 0 + fmt.Println(outErr.String()) + t.Fatal(err) + } +} diff --git a/validation/genesis/validation-inputs/10/meta.toml b/validation/genesis/validation-inputs/10/meta.toml new file mode 100644 index 000000000..4f555dab7 --- /dev/null +++ b/validation/genesis/validation-inputs/10/meta.toml @@ -0,0 +1,13 @@ +genesis_creation_commit = "b29868e89246274afa17062f9d91df19a3b7ef24" +genesis_creation_command = [ + "go", + "run", + "op-node/cmd/main.go", + "genesis", + "l2", + "--deploy-config=./packages/contracts-bedrock/deploy-config/mainnet.json", + "--outfile.l2=expected-genesis.json", + "--outfile.rollup=rollup.json", + "--l1-deployments=./packages/contracts-bedrock/deployments/mainnet", + "--l1-rpc=https://ethereum.publicnode.com/", +] diff --git a/validation/genesis/validation-inputs/34443/deploy-config.json b/validation/genesis/validation-inputs/34443/deploy-config.json new file mode 100644 index 000000000..9d105699c --- /dev/null +++ b/validation/genesis/validation-inputs/34443/deploy-config.json @@ -0,0 +1,50 @@ +{ + "l1StartingBlockTag": "0xf9b1b22a7ef9d13f063ea467bcb70fb6e9f29698ecb7366a2cdf5af2165cacee", + "l1ChainID": 1, + "l2ChainID": 34443, + "l2BlockTime": 2, + "finalizationPeriodSeconds": 604800, + "systemConfigOwner": "0x4a4962275DF8C60a80d3a25faEc5AA7De116A746", + "finalSystemOwner": "0x4a4962275DF8C60a80d3a25faEc5AA7De116A746", + "controller": "0xf4802485d882D8eEa73c8A07D7FaD3B20440f149", + "baseFeeVaultRecipient": "0xed4811010A86F7C39134fbC20206d906AD1176B6", + "l1FeeVaultRecipient": "0xed4811010A86F7C39134fbC20206d906AD1176B6", + "sequencerFeeVaultRecipient": "0xed4811010A86F7C39134fbC20206d906AD1176B6", + "l2GenesisBlockBaseFeePerGas": "0x3b9aca00", + "governanceTokenOwner": "0x4a4962275DF8C60a80d3a25faEc5AA7De116A746", + "governanceTokenSymbol": "OP", + "governanceTokenName": "Optimism", + "maxSequencerDrift": 600, + "sequencerWindowSize": 3600, + "channelTimeout": 300, + "p2pSequencerAddress": "0xa7fA9CA4ac88686A542C0f830d7378eAB4A0278F", + "optimismL2FeeRecipient": "0xB1498F5c779303Dc8A0A533197085ec77Faf9989", + "batchInboxAddress": "0x24E59d9d3Bd73ccC28Dc54062AF7EF7bFF58Bd67", + "batchSenderAddress": "0x99199a22125034c808ff20f377d91187E8050F2E", + "l2GenesisRegolithTimeOffset": "0x0", + "portalGuardian": "0x309Fe2536d01867018D120b40e4676723C53A14C", + "l2OutputOracleSubmissionInterval": 1800, + "l2OutputOracleStartingTimestamp": -1, + "l2OutputOracleStartingBlockNumber": "0x0", + "l2OutputOracleProposer": "0x674F64D64Ddc198db83cd9047dF54BF89cCD0ddB", + "l2OutputOracleOwner": "0x01409dB06A96EA7D10e81BcDD663D8bf745d2eab", + "sequencerFeeVaultWithdrawalNetwork": 0, + "baseFeeVaultWithdrawalNetwork": 0, + "l1FeeVaultWithdrawalNetwork": 0, + "baseFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", + "l1FeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", + "sequencerFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", + "l2GenesisBlockGasLimit": "0x1c9c380", + "fundDevAccounts": false, + "gasPriceOracleOverhead": 188, + "gasPriceOracleScalar": 684000, + "eip1559Denominator": 50, + "eip1559Elasticity": 6, + "proxyAdmin": "0xEeeEe4060b1785Da25634449DdC57B44A40457e7", + "proxyAdminOwner": "0xefCf0c8faFB425997870f845e26fC6cA6EE6dD5C", + "optimismBaseFeeRecipient": "0x821DE76f548C46C1D8bf1821E3d177FfB234Ae9D", + "optimismL1FeeRecipient": "0x8BB4202D5e710fBFc6303b39532821eDC28E79d2", + "l2CrossDomainMessengerOwner": "0x17e3Dd82dE9e63614eeC3294640c286e0736F591", + "gasPriceOracleOwner": "0x3Bd5af32fedf8Dc1A9BB60420F2FF0d0368e34D1", + "l2OutputOracleChallenger": "0x309Fe2536d01867018D120b40e4676723C53A14C" +} diff --git a/validation/genesis/validation-inputs/34443/meta.toml b/validation/genesis/validation-inputs/34443/meta.toml new file mode 100644 index 000000000..99497032a --- /dev/null +++ b/validation/genesis/validation-inputs/34443/meta.toml @@ -0,0 +1,13 @@ +genesis_creation_commit = "3eda4cd594ba6409584eb6ef95c341d98419e392" +genesis_creation_command = [ + "go", + "run", + "op-node/cmd/main.go", + "genesis", + "l2", + "--deploy-config=./packages/contracts-bedrock/deploy-config/34443.json", + "--outfile.l2=expected-genesis.json", + "--outfile.rollup=rollup.json", + "--deployment-dir=./packages/contracts-bedrock/deployments/34443", + "--l1-rpc=https://ethereum.publicnode.com/", +]