From a83c3752a4f29aa01b11c0c28a93937d07791cde Mon Sep 17 00:00:00 2001 From: Matthew Slipper Date: Wed, 2 Oct 2024 18:45:43 -0600 Subject: [PATCH] op-deployer: Test for existing OPCM (#12257) * op-deployer: Test for existing OPCM Adds a test for deployments against an existing OPCM. The test works by spinning up an Anvil instance and forking Sepolia. To run this test, you'll need to specify two env vars: - `SEPOLIA_RPC_URL`: RPC URL for a Sepolia node. - `ENABLE_ANVIL`: Set to `true` to enable the test. In CI, the test uses our internal CI RPC nodes. * goimports * ensure streams close * lint * run anvil as part of unit not integration tests * simplify * remove foundry from kurtosis * use auto mine * mount artifacts * redeploy OPCM * comment --- .circleci/config.yml | 19 ++- .../deployer/integration_test/apply_test.go | 84 ++++++++++++-- op-chain-ops/deployer/opcm/standard.go | 48 +++++++- op-service/testutils/anvil/anvil.go | 108 ++++++++++++++++++ 4 files changed, 244 insertions(+), 15 deletions(-) create mode 100644 op-service/testutils/anvil/anvil.go diff --git a/.circleci/config.yml b/.circleci/config.yml index ad9865d1a4ff..5d6d36496683 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -890,9 +890,14 @@ jobs: module: description: Go Module Name type: string + uses_artifacts: + description: Uses contract artifacts + type: boolean + default: false docker: - image: <> resource_class: xlarge + circleci_ip_ranges: true steps: - checkout - restore_cache: @@ -903,6 +908,10 @@ jobs: keys: - golang-build-cache-test-<>-{{ checksum "go.sum" }} - golang-build-cache-test- + - when: + condition: <> + steps: + - attach_workspace: { at: "." } - run: name: Install components command: | @@ -914,7 +923,7 @@ jobs: - run: name: run tests command: | - gotestsum --format=testname --junitfile=/tmp/test-results/<>.xml --jsonfile=/tmp/testlogs/log.json \ + ENABLE_ANVIL=true SEPOLIA_RPC_URL="https://ci-sepolia-l1.optimism.io" gotestsum --format=testname --junitfile=/tmp/test-results/<>.xml --jsonfile=/tmp/testlogs/log.json \ -- -parallel=8 -coverpkg=github.com/ethereum-optimism/optimism/... -coverprofile=coverage.out ./... working_directory: <> - save_cache: @@ -1441,7 +1450,6 @@ workflows: parameters: module: - op-batcher - - op-chain-ops - op-node - op-proposer - op-challenger @@ -1453,6 +1461,13 @@ workflows: - go-test: name: semver-natspec-tests module: packages/contracts-bedrock/scripts/checks/semver-natspec + - go-test: + name: op-chain-ops-tests + module: op-chain-ops + uses_artifacts: true + requires: + - go-mod-download + - contracts-bedrock-build - go-test-kurtosis: name: op-chain-ops-integration module: op-chain-ops diff --git a/op-chain-ops/deployer/integration_test/apply_test.go b/op-chain-ops/deployer/integration_test/apply_test.go index 184269618f0e..2f8e1d18c381 100644 --- a/op-chain-ops/deployer/integration_test/apply_test.go +++ b/op-chain-ops/deployer/integration_test/apply_test.go @@ -6,9 +6,14 @@ import ( "log/slog" "math/big" "net/url" + "os" "path" "runtime" "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-service/testutils/anvil" + crypto "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum-optimism/optimism/op-chain-ops/deployer" "github.com/holiman/uint256" @@ -65,11 +70,6 @@ func TestEndToEndApply(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - _, testFilename, _, ok := runtime.Caller(0) - require.Truef(t, ok, "failed to get test filename") - monorepoDir := path.Join(path.Dir(testFilename), "..", "..", "..") - artifactsDir := path.Join(monorepoDir, "packages", "contracts-bedrock", "forge-artifacts") - enclaveCtx := kurtosisutil.StartEnclave(t, ctx, lgr, "github.com/ethpandaops/ethereum-package", TestParams) service, err := enclaveCtx.GetServiceContext("el-1-geth-lighthouse") @@ -81,9 +81,6 @@ func TestEndToEndApply(t *testing.T) { l1Client, err := ethclient.Dial(rpcURL) require.NoError(t, err) - artifactsURL, err := url.Parse(fmt.Sprintf("file://%s", artifactsDir)) - require.NoError(t, err) - depKey := new(deployerKey) l1ChainID := big.NewInt(77799777) dk, err := devkeys.NewMnemonicDevKeys(devkeys.TestMnemonic) @@ -106,7 +103,7 @@ func TestEndToEndApply(t *testing.T) { } t.Run("initial chain", func(t *testing.T) { - intent, st := makeIntent(t, l1ChainID, artifactsURL, dk, id) + intent, st := makeIntent(t, l1ChainID, dk, id) require.NoError(t, deployer.ApplyPipeline( ctx, @@ -149,7 +146,7 @@ func TestEndToEndApply(t *testing.T) { t.Run("subsequent chain", func(t *testing.T) { newID := uint256.NewInt(2) - intent, st := makeIntent(t, l1ChainID, artifactsURL, dk, newID) + intent, st := makeIntent(t, l1ChainID, dk, newID) env.Workdir = t.TempDir() require.NoError(t, deployer.ApplyPipeline( @@ -182,10 +179,16 @@ func TestEndToEndApply(t *testing.T) { func makeIntent( t *testing.T, l1ChainID *big.Int, - artifactsURL *url.URL, dk *devkeys.MnemonicDevKeys, l2ChainID *uint256.Int, ) (*state.Intent, *state.State) { + _, testFilename, _, ok := runtime.Caller(0) + require.Truef(t, ok, "failed to get test filename") + monorepoDir := path.Join(path.Dir(testFilename), "..", "..", "..") + artifactsDir := path.Join(monorepoDir, "packages", "contracts-bedrock", "forge-artifacts") + artifactsURL, err := url.Parse(fmt.Sprintf("file://%s", artifactsDir)) + require.NoError(t, err) + addrFor := func(key devkeys.Key) common.Address { addr, err := dk.Address(key) require.NoError(t, err) @@ -261,3 +264,62 @@ func validateOPChainDeployment(t *testing.T, ctx context.Context, l1Client *ethc }) } } + +func TestApplyExistingOPCM(t *testing.T) { + anvil.Test(t) + + forkRPCUrl := os.Getenv("SEPOLIA_RPC_URL") + if forkRPCUrl == "" { + t.Skip("no fork RPC URL provided") + } + + lgr := testlog.Logger(t, slog.LevelDebug) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + runner, err := anvil.New( + forkRPCUrl, + lgr, + ) + require.NoError(t, err) + + require.NoError(t, runner.Start(ctx)) + t.Cleanup(func() { + require.NoError(t, runner.Stop()) + }) + + l1Client, err := ethclient.Dial(runner.RPCUrl()) + require.NoError(t, err) + + l1ChainID := big.NewInt(11155111) + dk, err := devkeys.NewMnemonicDevKeys(devkeys.TestMnemonic) + require.NoError(t, err) + // index 0 from Anvil's test set + priv, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") + require.NoError(t, err) + signer := opcrypto.SignerFnFromBind(opcrypto.PrivateKeySignerFn(priv, l1ChainID)) + deployerAddr := common.HexToAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266") + + l2ChainID := uint256.NewInt(1) + + env := &pipeline.Env{ + Workdir: t.TempDir(), + L1Client: l1Client, + Signer: signer, + Deployer: deployerAddr, + Logger: lgr, + } + + intent, st := makeIntent(t, l1ChainID, dk, l2ChainID) + intent.ContractsRelease = "op-contracts/v1.6.0" + + require.NoError(t, deployer.ApplyPipeline( + ctx, + env, + intent, + st, + )) + + validateOPChainDeployment(t, ctx, l1Client, st) +} diff --git a/op-chain-ops/deployer/opcm/standard.go b/op-chain-ops/deployer/opcm/standard.go index 51de8a483fa7..5a2a129f199a 100644 --- a/op-chain-ops/deployer/opcm/standard.go +++ b/op-chain-ops/deployer/opcm/standard.go @@ -4,6 +4,8 @@ import ( "embed" "fmt" + "github.com/BurntSushi/toml" + "github.com/ethereum-optimism/superchain-registry/superchain" "github.com/ethereum/go-ethereum/common" ) @@ -14,6 +16,36 @@ var StandardVersionsMainnetData string //go:embed standard-versions-sepolia.toml var StandardVersionsSepoliaData string +var StandardVersionsSepolia StandardVersions + +var StandardVersionsMainnet StandardVersions + +type StandardVersions struct { + Releases map[string]StandardVersionsReleases `toml:"releases"` +} + +type StandardVersionsReleases struct { + OptimismPortal StandardVersionRelease `toml:"optimism_portal"` + SystemConfig StandardVersionRelease `toml:"system_config"` + AnchorStateRegistry StandardVersionRelease `toml:"anchor_state_registry"` + DelayedWETH StandardVersionRelease `toml:"delayed_weth"` + DisputeGameFactory StandardVersionRelease `toml:"dispute_game_factory"` + FaultDisputeGame StandardVersionRelease `toml:"fault_dispute_game"` + PermissionedDisputeGame StandardVersionRelease `toml:"permissioned_dispute_game"` + MIPS StandardVersionRelease `toml:"mips"` + PreimageOracle StandardVersionRelease `toml:"preimage_oracle"` + L1CrossDomainMessenger StandardVersionRelease `toml:"l1_cross_domain_messenger"` + L1ERC721Bridge StandardVersionRelease `toml:"l1_erc721_bridge"` + L1StandardBridge StandardVersionRelease `toml:"l1_standard_bridge"` + OptimismMintableERC20Factory StandardVersionRelease `toml:"optimism_mintable_erc20_factory"` +} + +type StandardVersionRelease struct { + Version string `toml:"version"` + ImplementationAddress common.Address `toml:"implementation_address"` + Address common.Address `toml:"address"` +} + var _ embed.FS func StandardVersionsFor(chainID uint64) (string, error) { @@ -41,8 +73,8 @@ func SuperchainFor(chainID uint64) (*superchain.Superchain, error) { func ManagerImplementationAddrFor(chainID uint64) (common.Address, error) { switch chainID { case 11155111: - // Generated using the bootstrap command on 09/26/2024. - return common.HexToAddress("0x0dc727671d5c08e4e41e8909983ebfa6f57aa0bf"), nil + // Generated using the bootstrap command on 10/02/2024. + return common.HexToAddress("0x0f29118caed0f72873701bcc079398c594b6f8e4"), nil default: return common.Address{}, fmt.Errorf("unsupported chain ID: %d", chainID) } @@ -60,3 +92,15 @@ func ManagerOwnerAddrFor(chainID uint64) (common.Address, error) { return common.Address{}, fmt.Errorf("unsupported chain ID: %d", chainID) } } + +func init() { + StandardVersionsMainnet = StandardVersions{} + if err := toml.Unmarshal([]byte(StandardVersionsMainnetData), &StandardVersionsMainnet); err != nil { + panic(err) + } + + StandardVersionsSepolia = StandardVersions{} + if err := toml.Unmarshal([]byte(StandardVersionsSepoliaData), &StandardVersionsSepolia); err != nil { + panic(err) + } +} diff --git a/op-service/testutils/anvil/anvil.go b/op-service/testutils/anvil/anvil.go new file mode 100644 index 000000000000..c203712b3205 --- /dev/null +++ b/op-service/testutils/anvil/anvil.go @@ -0,0 +1,108 @@ +package anvil + +import ( + "bufio" + "context" + "fmt" + "io" + "os" + "os/exec" + "strconv" + "strings" + "sync" + "testing" + + "github.com/ethereum/go-ethereum/log" +) + +func Test(t *testing.T) { + if os.Getenv("ENABLE_ANVIL") == "" { + t.Skip("skipping Anvil test") + } +} + +const AnvilPort = 31967 + +type Runner struct { + proc *exec.Cmd + stdout io.ReadCloser + stderr io.ReadCloser + logger log.Logger + startedCh chan struct{} + wg sync.WaitGroup +} + +func New(l1RPCURL string, logger log.Logger) (*Runner, error) { + proc := exec.Command( + "anvil", + "--fork-url", l1RPCURL, + "--port", + strconv.Itoa(AnvilPort), + ) + stdout, err := proc.StdoutPipe() + if err != nil { + return nil, err + } + stderr, err := proc.StderrPipe() + if err != nil { + return nil, err + } + + return &Runner{ + proc: proc, + stdout: stdout, + stderr: stderr, + logger: logger, + startedCh: make(chan struct{}, 1), + }, nil +} + +func (r *Runner) Start(ctx context.Context) error { + if err := r.proc.Start(); err != nil { + return err + } + + r.wg.Add(2) + go r.outputStream(r.stdout) + go r.outputStream(r.stderr) + + select { + case <-r.startedCh: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +func (r *Runner) Stop() error { + err := r.proc.Process.Signal(os.Interrupt) + if err != nil { + return err + } + + // make sure the output streams close + defer r.wg.Wait() + return r.proc.Wait() +} + +func (r *Runner) outputStream(stream io.ReadCloser) { + defer r.wg.Done() + scanner := bufio.NewScanner(stream) + listenLine := fmt.Sprintf("Listening on 127.0.0.1:%d", AnvilPort) + started := sync.OnceFunc(func() { + r.startedCh <- struct{}{} + }) + + for scanner.Scan() { + line := scanner.Text() + if strings.Contains(line, listenLine) { + started() + } + + r.logger.Debug("[ANVIL] " + scanner.Text()) + } +} + +func (r *Runner) RPCUrl() string { + return fmt.Sprintf("http://localhost:%d", AnvilPort) +}