Skip to content

Commit

Permalink
add genesis predeploy test, validation metadata for chains 10,34443 a…
Browse files Browse the repository at this point in the history
…nd wire up
  • Loading branch information
geoknee committed Aug 22, 2024
1 parent 5ec1a6f commit f99b396
Show file tree
Hide file tree
Showing 11 changed files with 409 additions and 23 deletions.
27 changes: 27 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -241,6 +267,7 @@ workflows:
- golang-lint
- golang-modules-tidy
- golang-test
- golang-validate-genesis
- golang-validate-modified
- check-codegen
- cargo-tests
Expand Down
4 changes: 4 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 0 additions & 23 deletions superchain/superchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions validation/genesis/foundry-config.patch
Original file line number Diff line number Diff line change
@@ -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'
]
195 changes: 195 additions & 0 deletions validation/genesis/genesis-predeploy_test.go
Original file line number Diff line number Diff line change
@@ -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
}
56 changes: 56 additions & 0 deletions validation/genesis/genesis.go
Original file line number Diff line number Diff line change
@@ -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"`
}
15 changes: 15 additions & 0 deletions validation/genesis/monorepo-outputs.sh
Original file line number Diff line number Diff line change
@@ -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"
23 changes: 23 additions & 0 deletions validation/genesis/utils.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit f99b396

Please sign in to comment.