From 0c09dc8867693a8cd94a59cb576732acfd41ac81 Mon Sep 17 00:00:00 2001 From: Matthew Slipper Date: Fri, 6 Dec 2024 12:13:51 -0700 Subject: [PATCH 1/2] op-deployer: Bootstrap superchain command Fixes https://github.com/ethereum-optimism/optimism/issues/13265. --- op-deployer/pkg/deployer/bootstrap/flags.go | 57 +++++ .../pkg/deployer/bootstrap/opcm_test.go | 6 +- .../pkg/deployer/bootstrap/superchain.go | 218 ++++++++++++++++++ .../pkg/deployer/bootstrap/superchain_test.go | 95 ++++++++ 4 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 op-deployer/pkg/deployer/bootstrap/superchain.go create mode 100644 op-deployer/pkg/deployer/bootstrap/superchain_test.go diff --git a/op-deployer/pkg/deployer/bootstrap/flags.go b/op-deployer/pkg/deployer/bootstrap/flags.go index 20cf02b933761..3991829bd4602 100644 --- a/op-deployer/pkg/deployer/bootstrap/flags.go +++ b/op-deployer/pkg/deployer/bootstrap/flags.go @@ -35,6 +35,12 @@ const ( DelayedWethProxyFlagName = "delayed-weth-proxy" DelayedWethImplFlagName = "delayed-weth-impl" ProxyOwnerFlagName = "proxy-owner" + SuperchainProxyAdminOwnerFlagName = "superchain-proxy-admin-owner" + ProtocolVersionsOwnerFlagName = "protocol-versions-owner" + GuardianFlagName = "guardian" + PausedFlagName = "paused" + RequiredProtocolVersionFlagName = "required-protocol-version" + RecommendedProtocolVersionFlagName = "recommended-protocol-version" ) var ( @@ -176,6 +182,39 @@ var ( EnvVars: deployer.PrefixEnvVar("PROXY_OWNER"), Value: common.Address{}.Hex(), } + SuperchainProxyAdminOwnerFlag = &cli.StringFlag{ + Name: SuperchainProxyAdminOwnerFlagName, + Usage: "Owner address for the superchain proxy admin", + EnvVars: deployer.PrefixEnvVar("SUPERCHAIN_PROXY_ADMIN_OWNER"), + Value: common.Address{}.Hex(), + } + ProtocolVersionsOwnerFlag = &cli.StringFlag{ + Name: ProtocolVersionsOwnerFlagName, + Usage: "Owner address for protocol versions", + EnvVars: deployer.PrefixEnvVar("PROTOCOL_VERSIONS_OWNER"), + Value: common.Address{}.Hex(), + } + GuardianFlag = &cli.StringFlag{ + Name: GuardianFlagName, + Usage: "Guardian address", + EnvVars: deployer.PrefixEnvVar("GUARDIAN"), + Value: common.Address{}.Hex(), + } + PausedFlag = &cli.BoolFlag{ + Name: PausedFlagName, + Usage: "Initial paused state", + EnvVars: deployer.PrefixEnvVar("PAUSED"), + } + RequiredProtocolVersionFlag = &cli.StringFlag{ + Name: RequiredProtocolVersionFlagName, + Usage: "Required protocol version (semver)", + EnvVars: deployer.PrefixEnvVar("REQUIRED_PROTOCOL_VERSION"), + } + RecommendedProtocolVersionFlag = &cli.StringFlag{ + Name: RecommendedProtocolVersionFlagName, + Usage: "Recommended protocol version (semver)", + EnvVars: deployer.PrefixEnvVar("RECOMMENDED_PROTOCOL_VERSION"), + } ) var OPCMFlags = []cli.Flag{ @@ -239,6 +278,18 @@ var ProxyFlags = []cli.Flag{ ProxyOwnerFlag, } +var SuperchainFlags = []cli.Flag{ + deployer.L1RPCURLFlag, + deployer.PrivateKeyFlag, + ArtifactsLocatorFlag, + SuperchainProxyAdminOwnerFlag, + ProtocolVersionsOwnerFlag, + GuardianFlag, + PausedFlag, + RequiredProtocolVersionFlag, + RecommendedProtocolVersionFlag, +} + var Commands = []*cli.Command{ { Name: "opcm", @@ -285,4 +336,10 @@ var Commands = []*cli.Command{ Flags: cliapp.ProtectFlags(ProxyFlags), Action: ProxyCLI, }, + { + Name: "superchain", + Usage: "Bootstrap the Superchain configuration", + Flags: cliapp.ProtectFlags(SuperchainFlags), + Action: SuperchainCLI, + }, } diff --git a/op-deployer/pkg/deployer/bootstrap/opcm_test.go b/op-deployer/pkg/deployer/bootstrap/opcm_test.go index 5a4d1e8de3d51..f5b7fde5be9ad 100644 --- a/op-deployer/pkg/deployer/bootstrap/opcm_test.go +++ b/op-deployer/pkg/deployer/bootstrap/opcm_test.go @@ -17,7 +17,7 @@ import ( var networks = []string{"mainnet", "sepolia"} -var versions = []string{"v1.8.0-rc.3"} +var versions = []string{"v1.8.0-rc.3", "v1.6.0"} func TestOPCMLiveChain(t *testing.T) { for _, network := range networks { @@ -27,6 +27,10 @@ func TestOPCMLiveChain(t *testing.T) { t.Skip("v1.8.0-rc.3 not supported on mainnet yet") } + if version == "v1.6.0" && network == "sepolia" { + t.Skip("v1.6.0 not supported") + } + envVar := strings.ToUpper(network) + "_RPC_URL" rpcURL := os.Getenv(envVar) require.NotEmpty(t, rpcURL, "must specify RPC url via %s env var", envVar) diff --git a/op-deployer/pkg/deployer/bootstrap/superchain.go b/op-deployer/pkg/deployer/bootstrap/superchain.go new file mode 100644 index 0000000000000..837f406c091b1 --- /dev/null +++ b/op-deployer/pkg/deployer/bootstrap/superchain.go @@ -0,0 +1,218 @@ +package bootstrap + +import ( + "context" + "crypto/ecdsa" + "fmt" + "strings" + + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/env" + opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" + "github.com/ethereum-optimism/optimism/op-service/ctxinterrupt" + "github.com/ethereum-optimism/optimism/op-service/ioutil" + "github.com/ethereum-optimism/optimism/op-service/jsonutil" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" + "github.com/urfave/cli/v2" +) + +type SuperchainConfig struct { + L1RPCUrl string + PrivateKey string + Logger log.Logger + ArtifactsLocator *artifacts.Locator + + privateKeyECDSA *ecdsa.PrivateKey + + SuperchainProxyAdminOwner common.Address + ProtocolVersionsOwner common.Address + Guardian common.Address + Paused bool + RequiredProtocolVersion params.ProtocolVersion + RecommendedProtocolVersion params.ProtocolVersion +} + +func (c *SuperchainConfig) Check() error { + if c.L1RPCUrl == "" { + return fmt.Errorf("l1RPCUrl must be specified") + } + + if c.PrivateKey == "" { + return fmt.Errorf("private key must be specified") + } + + privECDSA, err := crypto.HexToECDSA(strings.TrimPrefix(c.PrivateKey, "0x")) + if err != nil { + return fmt.Errorf("failed to parse private key: %w", err) + } + c.privateKeyECDSA = privECDSA + + if c.Logger == nil { + return fmt.Errorf("logger must be specified") + } + + if c.ArtifactsLocator == nil { + return fmt.Errorf("artifacts locator must be specified") + } + + if c.SuperchainProxyAdminOwner == (common.Address{}) { + return fmt.Errorf("superchain proxy admin owner must be specified") + } + + if c.ProtocolVersionsOwner == (common.Address{}) { + return fmt.Errorf("protocol versions owner must be specified") + } + + if c.Guardian == (common.Address{}) { + return fmt.Errorf("guardian must be specified") + } + + return nil +} + +func SuperchainCLI(cliCtx *cli.Context) error { + logCfg := oplog.ReadCLIConfig(cliCtx) + l := oplog.NewLogger(oplog.AppOut(cliCtx), logCfg) + oplog.SetGlobalLogHandler(l.Handler()) + + l1RPCUrl := cliCtx.String(deployer.L1RPCURLFlagName) + privateKey := cliCtx.String(deployer.PrivateKeyFlagName) + artifactsURLStr := cliCtx.String(ArtifactsLocatorFlagName) + artifactsLocator := new(artifacts.Locator) + if err := artifactsLocator.UnmarshalText([]byte(artifactsURLStr)); err != nil { + return fmt.Errorf("failed to parse artifacts URL: %w", err) + } + + superchainProxyAdminOwner := common.HexToAddress(cliCtx.String(SuperchainProxyAdminOwnerFlagName)) + protocolVersionsOwner := common.HexToAddress(cliCtx.String(ProtocolVersionsOwnerFlagName)) + guardian := common.HexToAddress(cliCtx.String(GuardianFlagName)) + paused := cliCtx.Bool(PausedFlagName) + requiredVersionStr := cliCtx.String(RequiredProtocolVersionFlagName) + recommendedVersionStr := cliCtx.String(RecommendedProtocolVersionFlagName) + + cfg := SuperchainConfig{ + L1RPCUrl: l1RPCUrl, + PrivateKey: privateKey, + Logger: l, + ArtifactsLocator: artifactsLocator, + SuperchainProxyAdminOwner: superchainProxyAdminOwner, + ProtocolVersionsOwner: protocolVersionsOwner, + Guardian: guardian, + Paused: paused, + } + + if err := cfg.RequiredProtocolVersion.UnmarshalText([]byte(requiredVersionStr)); err != nil { + return fmt.Errorf("failed to parse required protocol version: %w", err) + } + if err := cfg.RecommendedProtocolVersion.UnmarshalText([]byte(recommendedVersionStr)); err != nil { + return fmt.Errorf("failed to parse required protocol version: %w", err) + } + + ctx := ctxinterrupt.WithCancelOnInterrupt(cliCtx.Context) + + dso, err := Superchain(ctx, cfg) + if err != nil { + return fmt.Errorf("failed to deploy superchain: %w", err) + } + + if err := jsonutil.WriteJSON(dso, ioutil.ToStdOut()); err != nil { + return fmt.Errorf("failed to write output: %w", err) + } + return nil +} + +func Superchain(ctx context.Context, cfg SuperchainConfig) (opcm.DeploySuperchainOutput, error) { + var dso opcm.DeploySuperchainOutput + + if err := cfg.Check(); err != nil { + return dso, fmt.Errorf("invalid config for Superchain: %w", err) + } + + lgr := cfg.Logger + progressor := func(curr, total int64) { + lgr.Info("artifacts download progress", "current", curr, "total", total) + } + + artifactsFS, cleanup, err := artifacts.Download(ctx, cfg.ArtifactsLocator, progressor) + if err != nil { + return dso, fmt.Errorf("failed to download artifacts: %w", err) + } + defer func() { + if err := cleanup(); err != nil { + lgr.Warn("failed to clean up artifacts", "err", err) + } + }() + + l1Client, err := ethclient.Dial(cfg.L1RPCUrl) + if err != nil { + return dso, fmt.Errorf("failed to connect to L1 RPC: %w", err) + } + + chainID, err := l1Client.ChainID(ctx) + if err != nil { + return dso, fmt.Errorf("failed to get chain ID: %w", err) + } + + signer := opcrypto.SignerFnFromBind(opcrypto.PrivateKeySignerFn(cfg.privateKeyECDSA, chainID)) + chainDeployer := crypto.PubkeyToAddress(cfg.privateKeyECDSA.PublicKey) + + bcaster, err := broadcaster.NewKeyedBroadcaster(broadcaster.KeyedBroadcasterOpts{ + Logger: lgr, + ChainID: chainID, + Client: l1Client, + Signer: signer, + From: chainDeployer, + }) + if err != nil { + return dso, fmt.Errorf("failed to create broadcaster: %w", err) + } + + l1RPC, err := rpc.Dial(cfg.L1RPCUrl) + if err != nil { + return dso, fmt.Errorf("failed to connect to L1 RPC: %w", err) + } + + l1Host, err := env.DefaultForkedScriptHost( + ctx, + bcaster, + lgr, + chainDeployer, + artifactsFS, + l1RPC, + ) + if err != nil { + return dso, fmt.Errorf("failed to create script host: %w", err) + } + + dso, err = opcm.DeploySuperchain( + l1Host, + opcm.DeploySuperchainInput{ + SuperchainProxyAdminOwner: cfg.SuperchainProxyAdminOwner, + ProtocolVersionsOwner: cfg.ProtocolVersionsOwner, + Guardian: cfg.Guardian, + Paused: cfg.Paused, + RequiredProtocolVersion: cfg.RequiredProtocolVersion, + RecommendedProtocolVersion: cfg.RecommendedProtocolVersion, + }, + ) + if err != nil { + return dso, fmt.Errorf("error deploying superchain: %w", err) + } + + if _, err := bcaster.Broadcast(ctx); err != nil { + return dso, fmt.Errorf("failed to broadcast: %w", err) + } + + lgr.Info("deployed superchain configuration") + + return dso, nil +} diff --git a/op-deployer/pkg/deployer/bootstrap/superchain_test.go b/op-deployer/pkg/deployer/bootstrap/superchain_test.go new file mode 100644 index 0000000000000..f2198102acb21 --- /dev/null +++ b/op-deployer/pkg/deployer/bootstrap/superchain_test.go @@ -0,0 +1,95 @@ +package bootstrap + +import ( + "context" + "log/slog" + "os" + "strings" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/retryproxy" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum-optimism/optimism/op-service/testutils/anvil" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" +) + +func TestSuperchain(t *testing.T) { + for _, network := range networks { + for _, version := range versions { + t.Run(network+"-"+version, func(t *testing.T) { + envVar := strings.ToUpper(network) + "_RPC_URL" + rpcURL := os.Getenv(envVar) + require.NotEmpty(t, rpcURL, "must specify RPC url via %s env var", envVar) + testSuperchain(t, rpcURL, version) + }) + } + } +} + +func testSuperchain(t *testing.T, forkRPCURL string, version string) { + t.Parallel() + + if forkRPCURL == "" { + t.Skip("forkRPCURL not set") + } + + lgr := testlog.Logger(t, slog.LevelDebug) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + retryProxy := retryproxy.New(lgr, forkRPCURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + runner, err := anvil.New( + retryProxy.Endpoint(), + lgr, + ) + require.NoError(t, err) + + require.NoError(t, runner.Start(ctx)) + t.Cleanup(func() { + require.NoError(t, runner.Stop()) + }) + + out, err := Superchain(ctx, SuperchainConfig{ + L1RPCUrl: runner.RPCUrl(), + PrivateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + ArtifactsLocator: artifacts.MustNewLocatorFromTag("op-contracts/" + version), + Logger: lgr, + + SuperchainProxyAdminOwner: common.Address{'S'}, + ProtocolVersionsOwner: common.Address{'P'}, + Guardian: common.Address{'G'}, + Paused: false, + RequiredProtocolVersion: params.ProtocolVersionV0{Major: 1}.Encode(), + RecommendedProtocolVersion: params.ProtocolVersionV0{Major: 2}.Encode(), + }) + require.NoError(t, err) + + client, err := ethclient.Dial(runner.RPCUrl()) + require.NoError(t, err) + + addresses := []common.Address{ + out.SuperchainConfigProxy, + out.SuperchainConfigImpl, + out.SuperchainProxyAdmin, + out.ProtocolVersionsImpl, + out.ProtocolVersionsProxy, + } + for _, addr := range addresses { + require.NotEmpty(t, addr) + + code, err := client.CodeAt(ctx, addr, nil) + require.NoError(t, err) + require.NotEmpty(t, code) + } +} From e08f7d4044cc0fd65a3493b66f9307c516464841 Mon Sep 17 00:00:00 2001 From: Matthew Slipper Date: Fri, 6 Dec 2024 12:23:25 -0700 Subject: [PATCH 2/2] fix test --- op-deployer/pkg/deployer/bootstrap/opcm_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op-deployer/pkg/deployer/bootstrap/opcm_test.go b/op-deployer/pkg/deployer/bootstrap/opcm_test.go index f5b7fde5be9ad..4ee089229dfcd 100644 --- a/op-deployer/pkg/deployer/bootstrap/opcm_test.go +++ b/op-deployer/pkg/deployer/bootstrap/opcm_test.go @@ -27,7 +27,7 @@ func TestOPCMLiveChain(t *testing.T) { t.Skip("v1.8.0-rc.3 not supported on mainnet yet") } - if version == "v1.6.0" && network == "sepolia" { + if version == "v1.6.0" { t.Skip("v1.6.0 not supported") }