Commit 395c01d0 authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

op-deployer: Replace old bootstrap scripts with DeployImplementations (#13742)

parent 49ea3d0b
...@@ -5,8 +5,6 @@ import ( ...@@ -5,8 +5,6 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -174,7 +172,6 @@ func DeploySuperchainToL1(l1Host *script.Host, superCfg *SuperchainConfig) (*Sup ...@@ -174,7 +172,6 @@ func DeploySuperchainToL1(l1Host *script.Host, superCfg *SuperchainConfig) (*Sup
SuperchainConfigProxy: superDeployment.SuperchainConfigProxy, SuperchainConfigProxy: superDeployment.SuperchainConfigProxy,
ProtocolVersionsProxy: superDeployment.ProtocolVersionsProxy, ProtocolVersionsProxy: superDeployment.ProtocolVersionsProxy,
UseInterop: superCfg.Implementations.UseInterop, UseInterop: superCfg.Implementations.UseInterop,
StandardVersionsToml: standard.VersionsMainnetData,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to deploy Implementations contracts: %w", err) return nil, fmt.Errorf("failed to deploy Implementations contracts: %w", err)
......
package bootstrap
import (
"context"
"crypto/ecdsa"
"fmt"
"math/big"
"strings"
artifacts2 "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/env"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
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/rpc"
"github.com/urfave/cli/v2"
)
type DelayedWETHConfig struct {
L1RPCUrl string
PrivateKey string
Logger log.Logger
ArtifactsLocator *artifacts2.Locator
DelayedWethImpl common.Address
privateKeyECDSA *ecdsa.PrivateKey
}
func (c *DelayedWETHConfig) 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")
}
return nil
}
func DelayedWETHCLI(cliCtx *cli.Context) error {
logCfg := oplog.ReadCLIConfig(cliCtx)
l := oplog.NewLogger(oplog.AppOut(cliCtx), logCfg)
oplog.SetGlobalLogHandler(l.Handler())
outfile := cliCtx.String(OutfileFlagName)
config, err := NewDelayedWETHConfigFromClI(cliCtx, l)
if err != nil {
return err
}
ctx := ctxinterrupt.WithCancelOnInterrupt(cliCtx.Context)
dwo, err := DelayedWETH(ctx, config)
if err != nil {
return fmt.Errorf("failed to deploy DelayedWETH: %w", err)
}
if err := jsonutil.WriteJSON(dwo, ioutil.ToStdOutOrFileOrNoop(outfile, 0o755)); err != nil {
return fmt.Errorf("failed to write output: %w", err)
}
return nil
}
func NewDelayedWETHConfigFromClI(cliCtx *cli.Context, l log.Logger) (DelayedWETHConfig, error) {
l1RPCUrl := cliCtx.String(deployer.L1RPCURLFlagName)
privateKey := cliCtx.String(deployer.PrivateKeyFlagName)
artifactsURLStr := cliCtx.String(ArtifactsLocatorFlagName)
artifactsLocator := new(artifacts2.Locator)
if err := artifactsLocator.UnmarshalText([]byte(artifactsURLStr)); err != nil {
return DelayedWETHConfig{}, fmt.Errorf("failed to parse artifacts URL: %w", err)
}
delayedWethImpl := common.HexToAddress(cliCtx.String(DelayedWethImplFlagName))
config := DelayedWETHConfig{
L1RPCUrl: l1RPCUrl,
PrivateKey: privateKey,
Logger: l,
ArtifactsLocator: artifactsLocator,
DelayedWethImpl: delayedWethImpl,
}
return config, nil
}
func DelayedWETH(ctx context.Context, cfg DelayedWETHConfig) (opcm.DeployDelayedWETHOutput, error) {
var dwo opcm.DeployDelayedWETHOutput
if err := cfg.Check(); err != nil {
return dwo, fmt.Errorf("invalid config for DelayedWETH: %w", err)
}
lgr := cfg.Logger
progressor := func(curr, total int64) {
lgr.Info("artifacts download progress", "current", curr, "total", total)
}
artifactsFS, cleanup, err := artifacts2.Download(ctx, cfg.ArtifactsLocator, progressor)
if err != nil {
return dwo, 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 dwo, fmt.Errorf("failed to connect to L1 RPC: %w", err)
}
chainID, err := l1Client.ChainID(ctx)
if err != nil {
return dwo, fmt.Errorf("failed to get chain ID: %w", err)
}
chainIDU64 := chainID.Uint64()
superCfg, err := standard.SuperchainFor(chainIDU64)
if err != nil {
return dwo, fmt.Errorf("error getting superchain config: %w", err)
}
proxyAdmin, err := standard.SuperchainProxyAdminAddrFor(chainIDU64)
if err != nil {
return dwo, fmt.Errorf("error getting superchain proxy admin: %w", err)
}
delayedWethOwner, err := standard.SystemOwnerAddrFor(chainIDU64)
if err != nil {
return dwo, fmt.Errorf("error getting superchain system owner: %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 dwo, fmt.Errorf("failed to create broadcaster: %w", err)
}
l1RPC, err := rpc.Dial(cfg.L1RPCUrl)
if err != nil {
return dwo, fmt.Errorf("failed to connect to L1 RPC: %w", err)
}
host, err := env.DefaultForkedScriptHost(
ctx,
bcaster,
lgr,
chainDeployer,
artifactsFS,
l1RPC,
)
if err != nil {
return dwo, fmt.Errorf("failed to create script host: %w", err)
}
var release string
if cfg.ArtifactsLocator.IsTag() {
release = cfg.ArtifactsLocator.Tag
} else {
release = "dev"
}
lgr.Info("deploying DelayedWETH", "release", release)
superchainConfigAddr := common.Address(*superCfg.Config.SuperchainConfigAddr)
dwo, err = opcm.DeployDelayedWETH(
host,
opcm.DeployDelayedWETHInput{
Release: release,
ProxyAdmin: proxyAdmin,
SuperchainConfigProxy: superchainConfigAddr,
DelayedWethImpl: cfg.DelayedWethImpl,
DelayedWethOwner: delayedWethOwner,
DelayedWethDelay: big.NewInt(604800),
},
)
if err != nil {
return dwo, fmt.Errorf("error deploying DelayedWETH: %w", err)
}
if _, err := bcaster.Broadcast(ctx); err != nil {
return dwo, fmt.Errorf("failed to broadcast: %w", err)
}
lgr.Info("deployed DelayedWETH")
return dwo, nil
}
package bootstrap
import (
"testing"
"github.com/ethereum-optimism/optimism/op-service/cliapp"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
)
func TestNewDelayedWETHConfigFromCLI(t *testing.T) {
ctx, err := parseCLIArgs(DelayedWETHFlags,
"--artifacts-locator", "tag://op-contracts/v1.6.0",
"--l1-rpc-url", "http://foo",
"--private-key", "0x123456")
require.NoError(t, err)
logger := testlog.Logger(t, log.LvlInfo)
cfg, err := NewDelayedWETHConfigFromClI(ctx, logger)
require.NoError(t, err)
require.Same(t, logger, cfg.Logger)
require.Equal(t, "op-contracts/v1.6.0", cfg.ArtifactsLocator.Tag)
require.True(t, cfg.ArtifactsLocator.IsTag())
require.Equal(t, "0x123456", cfg.PrivateKey)
}
func parseCLIArgs(flags []cli.Flag, args ...string) (*cli.Context, error) {
app := cli.NewApp()
app.Flags = cliapp.ProtectFlags(flags)
var ctx *cli.Context
app.Action = func(c *cli.Context) error {
ctx = c
return nil
}
argsWithCmd := make([]string, len(args)+1)
argsWithCmd[0] = "bootstrap"
copy(argsWithCmd[1:], args)
err := app.Run(argsWithCmd)
if err != nil {
return nil, err
}
return ctx, nil
}
package bootstrap
import (
"context"
"crypto/ecdsa"
"fmt"
"math/big"
"strings"
artifacts2 "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/env"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
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/urfave/cli/v2"
)
type DisputeGameConfig struct {
L1RPCUrl string
PrivateKey string
Logger log.Logger
ArtifactsLocator *artifacts2.Locator
privateKeyECDSA *ecdsa.PrivateKey
Vm common.Address
GameKind string
GameType uint32
AbsolutePrestate common.Hash
MaxGameDepth uint64
SplitDepth uint64
ClockExtension uint64
MaxClockDuration uint64
DelayedWethProxy common.Address
AnchorStateRegistryProxy common.Address
L2ChainId uint64
Proposer common.Address
Challenger common.Address
}
func (c *DisputeGameConfig) 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")
}
return nil
}
func DisputeGameCLI(cliCtx *cli.Context) error {
logCfg := oplog.ReadCLIConfig(cliCtx)
l := oplog.NewLogger(oplog.AppOut(cliCtx), logCfg)
oplog.SetGlobalLogHandler(l.Handler())
outfile := cliCtx.String(OutfileFlagName)
cfg, err := NewDisputeGameConfigFromCLI(cliCtx, l)
if err != nil {
return err
}
ctx := ctxinterrupt.WithCancelOnInterrupt(cliCtx.Context)
dgo, err := DisputeGame(ctx, cfg)
if err != nil {
return fmt.Errorf("failed to deploy dispute game: %w", err)
}
if err := jsonutil.WriteJSON(dgo, ioutil.ToStdOutOrFileOrNoop(outfile, 0o755)); err != nil {
return fmt.Errorf("failed to write output: %w", err)
}
return nil
}
func NewDisputeGameConfigFromCLI(cliCtx *cli.Context, l log.Logger) (DisputeGameConfig, error) {
l1RPCUrl := cliCtx.String(deployer.L1RPCURLFlagName)
privateKey := cliCtx.String(deployer.PrivateKeyFlagName)
artifactsURLStr := cliCtx.String(ArtifactsLocatorFlagName)
artifactsLocator := new(artifacts2.Locator)
if err := artifactsLocator.UnmarshalText([]byte(artifactsURLStr)); err != nil {
return DisputeGameConfig{}, fmt.Errorf("failed to parse artifacts URL: %w", err)
}
cfg := DisputeGameConfig{
L1RPCUrl: l1RPCUrl,
PrivateKey: privateKey,
Logger: l,
ArtifactsLocator: artifactsLocator,
Vm: common.HexToAddress(cliCtx.String(VmFlagName)),
GameKind: cliCtx.String(GameKindFlagName),
GameType: uint32(cliCtx.Uint64(GameTypeFlagName)),
AbsolutePrestate: common.HexToHash(cliCtx.String(AbsolutePrestateFlagName)),
MaxGameDepth: cliCtx.Uint64(MaxGameDepthFlagName),
SplitDepth: cliCtx.Uint64(SplitDepthFlagName),
ClockExtension: cliCtx.Uint64(ClockExtensionFlagName),
MaxClockDuration: cliCtx.Uint64(MaxClockDurationFlagName),
DelayedWethProxy: common.HexToAddress(cliCtx.String(DelayedWethProxyFlagName)),
AnchorStateRegistryProxy: common.HexToAddress(cliCtx.String(AnchorStateRegistryProxyFlagName)),
L2ChainId: cliCtx.Uint64(L2ChainIdFlagName),
Proposer: common.HexToAddress(cliCtx.String(ProposerFlagName)),
Challenger: common.HexToAddress(cliCtx.String(ChallengerFlagName)),
}
return cfg, nil
}
func DisputeGame(ctx context.Context, cfg DisputeGameConfig) (opcm.DeployDisputeGameOutput, error) {
var dgo opcm.DeployDisputeGameOutput
if err := cfg.Check(); err != nil {
return dgo, fmt.Errorf("invalid config for DisputeGame: %w", err)
}
lgr := cfg.Logger
progressor := func(curr, total int64) {
lgr.Info("artifacts download progress", "current", curr, "total", total)
}
artifactsFS, cleanup, err := artifacts2.Download(ctx, cfg.ArtifactsLocator, progressor)
if err != nil {
return dgo, 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 dgo, fmt.Errorf("failed to connect to L1 RPC: %w", err)
}
l1Rpc, err := rpc.Dial(cfg.L1RPCUrl)
if err != nil {
return dgo, fmt.Errorf("failed to connect to L1 RPC: %w", err)
}
chainID, err := l1Client.ChainID(ctx)
if err != nil {
return dgo, fmt.Errorf("failed to get chain ID: %w", err)
}
chainIDU64 := chainID.Uint64()
standardVersionsTOML, err := standard.L1VersionsDataFor(chainIDU64)
if err != nil {
return dgo, fmt.Errorf("error getting standard versions TOML: %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 dgo, fmt.Errorf("failed to create broadcaster: %w", err)
}
host, err := env.DefaultForkedScriptHost(
ctx,
bcaster,
lgr,
chainDeployer,
artifactsFS,
l1Rpc,
)
if err != nil {
return dgo, fmt.Errorf("failed to create L1 script host: %w", err)
}
var release string
if cfg.ArtifactsLocator.IsTag() {
release = cfg.ArtifactsLocator.Tag
} else {
release = "dev"
}
lgr.Info("deploying dispute game", "release", release)
dgo, err = opcm.DeployDisputeGame(
host,
opcm.DeployDisputeGameInput{
Release: release,
StandardVersionsToml: standardVersionsTOML,
VmAddress: cfg.Vm,
GameKind: cfg.GameKind,
GameType: cfg.GameType,
AbsolutePrestate: cfg.AbsolutePrestate,
MaxGameDepth: cfg.MaxGameDepth,
SplitDepth: cfg.SplitDepth,
ClockExtension: cfg.ClockExtension,
MaxClockDuration: cfg.MaxClockDuration,
DelayedWethProxy: cfg.DelayedWethProxy,
AnchorStateRegistryProxy: cfg.AnchorStateRegistryProxy,
L2ChainId: common.BigToHash(new(big.Int).SetUint64(cfg.L2ChainId)),
Proposer: cfg.Proposer,
Challenger: cfg.Challenger,
},
)
if err != nil {
return dgo, fmt.Errorf("error deploying dispute game: %w", err)
}
if _, err := bcaster.Broadcast(ctx); err != nil {
return dgo, fmt.Errorf("failed to broadcast: %w", err)
}
lgr.Info("deployed dispute game")
return dgo, nil
}
package bootstrap
import (
"reflect"
"testing"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
func TestNewDisputeGameConfigFromCLI(t *testing.T) {
ctx, err := parseCLIArgs(DisputeGameFlags,
"--artifacts-locator", "tag://op-contracts/v1.6.0",
"--l1-rpc-url", "http://foo",
"--private-key", "0x123456",
"--game-type", "2",
"--delayed-weth-proxy", common.Address{0xaa}.Hex(),
"--anchor-state-registry-proxy", common.Address{0xbb}.Hex(),
"--l2-chain-id", "901",
"--proposer", common.Address{0xcc}.Hex(),
"--challenger", common.Address{0xdd}.Hex(),
"--vm", common.Address{0xee}.Hex(),
)
require.NoError(t, err)
logger := testlog.Logger(t, log.LvlInfo)
cfg, err := NewDisputeGameConfigFromCLI(ctx, logger)
require.NoError(t, err)
require.Same(t, logger, cfg.Logger)
require.Equal(t, "op-contracts/v1.6.0", cfg.ArtifactsLocator.Tag)
require.True(t, cfg.ArtifactsLocator.IsTag())
require.Equal(t, "0x123456", cfg.PrivateKey)
require.Equal(t, "FaultDisputeGame", cfg.GameKind)
require.Equal(t, uint32(2), cfg.GameType)
require.Equal(t, standard.DisputeAbsolutePrestate, cfg.AbsolutePrestate)
require.Equal(t, standard.DisputeMaxGameDepth, cfg.MaxGameDepth)
require.Equal(t, standard.DisputeSplitDepth, cfg.SplitDepth)
require.Equal(t, standard.DisputeClockExtension, cfg.ClockExtension)
require.Equal(t, standard.DisputeMaxClockDuration, cfg.MaxClockDuration)
require.Equal(t, common.Address{0xaa}, cfg.DelayedWethProxy)
require.Equal(t, common.Address{0xbb}, cfg.AnchorStateRegistryProxy)
require.Equal(t, common.Address{0xcc}, cfg.Proposer)
require.Equal(t, common.Address{0xdd}, cfg.Challenger)
require.Equal(t, common.Address{0xee}, cfg.Vm)
require.Equal(t, uint64(901), cfg.L2ChainId)
// Check all fields are set to ensure any newly added fields don't get missed.
cfgRef := reflect.ValueOf(cfg)
cfgType := reflect.TypeOf(cfg)
var unsetFields []string
for i := 0; i < cfgRef.NumField(); i++ {
field := cfgType.Field(i)
if field.Type == reflect.TypeOf(cfg.privateKeyECDSA) {
// privateKeyECDSA is only set when Check() is called so skip it.
continue
}
if cfgRef.Field(i).IsZero() {
unsetFields = append(unsetFields, field.Name)
}
}
require.Empty(t, unsetFields, "Found unset fields in config")
}
package bootstrap package bootstrap
import ( import (
"errors"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum-optimism/optimism/op-service/cliapp" "github.com/ethereum-optimism/optimism/op-service/cliapp"
...@@ -19,22 +17,6 @@ const ( ...@@ -19,22 +17,6 @@ const (
ProofMaturityDelaySecondsFlagName = "proof-maturity-delay-seconds" ProofMaturityDelaySecondsFlagName = "proof-maturity-delay-seconds"
DisputeGameFinalityDelaySecondsFlagName = "dispute-game-finality-delay-seconds" DisputeGameFinalityDelaySecondsFlagName = "dispute-game-finality-delay-seconds"
MIPSVersionFlagName = "mips-version" MIPSVersionFlagName = "mips-version"
VmFlagName = "vm"
GameKindFlagName = "game-kind"
GameTypeFlagName = "game-type"
AbsolutePrestateFlagName = "absolute-prestate"
MaxGameDepthFlagName = "max-game-depth"
SplitDepthFlagName = "split-depth"
ClockExtensionFlagName = "clock-extension"
MaxClockDurationFlagName = "max-clock-duration"
AnchorStateRegistryProxyFlagName = "anchor-state-registry-proxy"
L2ChainIdFlagName = "l2-chain-id"
ProposerFlagName = "proposer"
ChallengerFlagName = "challenger"
PreimageOracleFlagName = "preimage-oracle"
ReleaseFlagName = "release"
DelayedWethProxyFlagName = "delayed-weth-proxy"
DelayedWethImplFlagName = "delayed-weth-impl"
ProxyOwnerFlagName = "proxy-owner" ProxyOwnerFlagName = "proxy-owner"
SuperchainProxyAdminOwnerFlagName = "superchain-proxy-admin-owner" SuperchainProxyAdminOwnerFlagName = "superchain-proxy-admin-owner"
ProtocolVersionsOwnerFlagName = "protocol-versions-owner" ProtocolVersionsOwnerFlagName = "protocol-versions-owner"
...@@ -92,97 +74,6 @@ var ( ...@@ -92,97 +74,6 @@ var (
EnvVars: deployer.PrefixEnvVar("MIPS_VERSION"), EnvVars: deployer.PrefixEnvVar("MIPS_VERSION"),
Value: standard.MIPSVersion, Value: standard.MIPSVersion,
} }
VmFlag = &cli.StringFlag{
Name: VmFlagName,
Usage: "VM contract address.",
EnvVars: deployer.PrefixEnvVar("VM"),
}
GameKindFlag = &cli.StringFlag{
Name: GameKindFlagName,
Usage: "Game kind (FaultDisputeGame or PermissionedDisputeGame).",
EnvVars: deployer.PrefixEnvVar("GAME_KIND"),
Value: "FaultDisputeGame",
}
GameTypeFlag = &cli.StringFlag{
Name: GameTypeFlagName,
Usage: "Game type (integer or fractional).",
EnvVars: deployer.PrefixEnvVar("GAME_TYPE"),
}
AbsolutePrestateFlag = &cli.StringFlag{
Name: AbsolutePrestateFlagName,
Usage: "Absolute prestate.",
EnvVars: deployer.PrefixEnvVar("ABSOLUTE_PRESTATE"),
Value: standard.DisputeAbsolutePrestate.Hex(),
}
MaxGameDepthFlag = &cli.Uint64Flag{
Name: MaxGameDepthFlagName,
Usage: "Max game depth.",
EnvVars: deployer.PrefixEnvVar("MAX_GAME_DEPTH"),
Value: standard.DisputeMaxGameDepth,
}
SplitDepthFlag = &cli.Uint64Flag{
Name: SplitDepthFlagName,
Usage: "Split depth.",
EnvVars: deployer.PrefixEnvVar("SPLIT_DEPTH"),
Value: standard.DisputeSplitDepth,
}
ClockExtensionFlag = &cli.Uint64Flag{
Name: ClockExtensionFlagName,
Usage: "Clock extension.",
EnvVars: deployer.PrefixEnvVar("CLOCK_EXTENSION"),
Value: standard.DisputeClockExtension,
}
MaxClockDurationFlag = &cli.Uint64Flag{
Name: MaxClockDurationFlagName,
Usage: "Max clock duration.",
EnvVars: deployer.PrefixEnvVar("MAX_CLOCK_DURATION"),
Value: standard.DisputeMaxClockDuration,
}
DelayedWethProxyFlag = &cli.StringFlag{
Name: DelayedWethProxyFlagName,
Usage: "Delayed WETH proxy.",
EnvVars: deployer.PrefixEnvVar("DELAYED_WETH_PROXY"),
}
DelayedWethImplFlag = &cli.StringFlag{
Name: DelayedWethImplFlagName,
Usage: "Delayed WETH implementation.",
EnvVars: deployer.PrefixEnvVar("DELAYED_WETH_IMPL"),
Value: common.Address{}.Hex(),
}
AnchorStateRegistryProxyFlag = &cli.StringFlag{
Name: AnchorStateRegistryProxyFlagName,
Usage: "Anchor state registry proxy.",
EnvVars: deployer.PrefixEnvVar("ANCHOR_STATE_REGISTRY_PROXY"),
}
L2ChainIdFlag = &cli.Uint64Flag{
Name: L2ChainIdFlagName,
Usage: "L2 chain ID.",
EnvVars: deployer.PrefixEnvVar("L2_CHAIN_ID"),
}
ProposerFlag = &cli.StringFlag{
Name: ProposerFlagName,
Usage: "Proposer address (permissioned game only).",
EnvVars: deployer.PrefixEnvVar("PROPOSER"),
Value: common.Address{}.Hex(),
}
ChallengerFlag = &cli.StringFlag{
Name: ChallengerFlagName,
Usage: "Challenger address (permissioned game only).",
EnvVars: deployer.PrefixEnvVar("CHALLENGER"),
Value: common.Address{}.Hex(),
}
PreimageOracleFlag = &cli.StringFlag{
Name: PreimageOracleFlagName,
Usage: "Preimage oracle address.",
EnvVars: deployer.PrefixEnvVar("PREIMAGE_ORACLE"),
Value: common.Address{}.Hex(),
}
ReleaseFlag = &cli.StringFlag{
Name: ReleaseFlagName,
Usage: "Release to deploy.",
EnvVars: deployer.PrefixEnvVar("RELEASE"),
Value: common.Address{}.Hex(),
}
ProxyOwnerFlag = &cli.StringFlag{ ProxyOwnerFlag = &cli.StringFlag{
Name: ProxyOwnerFlagName, Name: ProxyOwnerFlagName,
Usage: "Proxy owner address.", Usage: "Proxy owner address.",
...@@ -222,64 +113,45 @@ var ( ...@@ -222,64 +113,45 @@ var (
Usage: "Recommended protocol version (semver)", Usage: "Recommended protocol version (semver)",
EnvVars: deployer.PrefixEnvVar("RECOMMENDED_PROTOCOL_VERSION"), EnvVars: deployer.PrefixEnvVar("RECOMMENDED_PROTOCOL_VERSION"),
} }
L1ContractsReleaseFlag = &cli.StringFlag{
Name: "l1-contracts-release",
Usage: "Release version to set OPCM implementations for, of the format `op-contracts/vX.Y.Z`.",
EnvVars: deployer.PrefixEnvVar("L1_CONTRACTS_RELEASE"),
}
SuperchainConfigProxyFlag = &cli.StringFlag{
Name: "superchain-config-proxy",
Usage: "Superchain config proxy.",
EnvVars: deployer.PrefixEnvVar("SUPERCHAIN_CONFIG_PROXY"),
}
ProtocolVersionsProxyFlag = &cli.StringFlag{
Name: "protocol-versions-proxy",
Usage: "Protocol versions proxy.",
EnvVars: deployer.PrefixEnvVar("PROTOCOL_VERSIONS_PROXY"),
}
UseInteropFlag = &cli.BoolFlag{
Name: "use-interop",
Usage: "If true, deploy Interop implementations.",
EnvVars: deployer.PrefixEnvVar("USE_INTEROP"),
}
) )
var OPCMFlags = []cli.Flag{ var ImplementationsFlags = []cli.Flag{
deployer.L1RPCURLFlag, deployer.L1RPCURLFlag,
deployer.PrivateKeyFlag, deployer.PrivateKeyFlag,
ReleaseFlag,
OutfileFlag, OutfileFlag,
} ArtifactsLocatorFlag,
L1ContractsReleaseFlag,
var ImplementationsFlags = []cli.Flag{
MIPSVersionFlag, MIPSVersionFlag,
WithdrawalDelaySecondsFlag, WithdrawalDelaySecondsFlag,
MinProposalSizeBytesFlag, MinProposalSizeBytesFlag,
ChallengePeriodSecondsFlag, ChallengePeriodSecondsFlag,
ProofMaturityDelaySecondsFlag, ProofMaturityDelaySecondsFlag,
DisputeGameFinalityDelaySecondsFlag, DisputeGameFinalityDelaySecondsFlag,
SuperchainConfigProxyFlag,
ProtocolVersionsProxyFlag,
UseInteropFlag,
} }
var DelayedWETHFlags = []cli.Flag{
deployer.L1RPCURLFlag,
deployer.PrivateKeyFlag,
OutfileFlag,
ArtifactsLocatorFlag,
DelayedWethImplFlag,
}
var DisputeGameFlags = []cli.Flag{
deployer.L1RPCURLFlag,
deployer.PrivateKeyFlag,
OutfileFlag,
ArtifactsLocatorFlag,
VmFlag,
GameKindFlag,
GameTypeFlag,
AbsolutePrestateFlag,
MaxGameDepthFlag,
SplitDepthFlag,
ClockExtensionFlag,
MaxClockDurationFlag,
DelayedWethProxyFlag,
AnchorStateRegistryProxyFlag,
L2ChainIdFlag,
ProposerFlag,
ChallengerFlag,
}
var BaseFPVMFlags = []cli.Flag{
deployer.L1RPCURLFlag,
deployer.PrivateKeyFlag,
OutfileFlag,
ArtifactsLocatorFlag,
PreimageOracleFlag,
}
var MIPSFlags = append(BaseFPVMFlags, MIPSVersionFlag)
var AsteriscFlags = BaseFPVMFlags
var ProxyFlags = []cli.Flag{ var ProxyFlags = []cli.Flag{
deployer.L1RPCURLFlag, deployer.L1RPCURLFlag,
deployer.PrivateKeyFlag, deployer.PrivateKeyFlag,
...@@ -302,45 +174,13 @@ var SuperchainFlags = []cli.Flag{ ...@@ -302,45 +174,13 @@ var SuperchainFlags = []cli.Flag{
} }
var Commands = []*cli.Command{ var Commands = []*cli.Command{
{
Name: "opcm",
Usage: "Bootstrap an instance of OPCM.",
Flags: cliapp.ProtectFlags(OPCMFlags),
Action: OPCMCLI,
},
{ {
Name: "implementations", Name: "implementations",
Usage: "Bootstraps implementations.", Usage: "Bootstraps implementations.",
Flags: cliapp.ProtectFlags(ImplementationsFlags), Flags: cliapp.ProtectFlags(ImplementationsFlags),
Action: func(context *cli.Context) error { Action: ImplementationsCLI,
return errors.New("not implemented yet")
},
Hidden: true, Hidden: true,
}, },
{
Name: "delayedweth",
Usage: "Bootstrap an instance of DelayedWETH.",
Flags: cliapp.ProtectFlags(DelayedWETHFlags),
Action: DelayedWETHCLI,
},
{
Name: "disputegame",
Usage: "Bootstrap an instance of a FaultDisputeGame or PermissionedDisputeGame.",
Flags: cliapp.ProtectFlags(DisputeGameFlags),
Action: DisputeGameCLI,
},
{
Name: "mips",
Usage: "Bootstrap an instance of MIPS.",
Flags: cliapp.ProtectFlags(MIPSFlags),
Action: MIPSCLI,
},
{
Name: "asterisc",
Usage: "Bootstrap an instance of Asterisc.",
Flags: cliapp.ProtectFlags(AsteriscFlags),
Action: AsteriscCLI,
},
{ {
Name: "proxy", Name: "proxy",
Usage: "Bootstrap a ERC-1967 Proxy without an implementation set.", Usage: "Bootstrap a ERC-1967 Proxy without an implementation set.",
......
...@@ -3,23 +3,22 @@ package bootstrap ...@@ -3,23 +3,22 @@ package bootstrap
import ( import (
"context" "context"
"crypto/ecdsa" "crypto/ecdsa"
"errors"
"fmt" "fmt"
"math/big"
"strings" "strings"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/env"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/env"
"github.com/ethereum-optimism/optimism/op-service/cliutil"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
"github.com/ethereum-optimism/optimism/op-service/ctxinterrupt" "github.com/ethereum-optimism/optimism/op-service/ctxinterrupt"
"github.com/ethereum-optimism/optimism/op-service/ioutil" "github.com/ethereum-optimism/optimism/op-service/ioutil"
"github.com/ethereum-optimism/optimism/op-service/jsonutil" "github.com/ethereum-optimism/optimism/op-service/jsonutil"
oplog "github.com/ethereum-optimism/optimism/op-service/log" 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/crypto"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -27,24 +26,32 @@ import ( ...@@ -27,24 +26,32 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
type AsteriscConfig struct { type ImplementationsConfig struct {
L1RPCUrl string L1RPCUrl string `cli:"l1-rpc-url"`
PrivateKey string PrivateKey string `cli:"private-key"`
ArtifactsLocator *artifacts.Locator `cli:"artifacts-locator"`
L1ContractsRelease string `cli:"l1-contracts-release"`
MIPSVersion int `cli:"mips-version"`
WithdrawalDelaySeconds uint64 `cli:"withdrawal-delay-seconds"`
MinProposalSizeBytes uint64 `cli:"min-proposal-size-bytes"`
ChallengePeriodSeconds uint64 `cli:"challenge-period-seconds"`
ProofMaturityDelaySeconds uint64 `cli:"proof-maturity-delay-seconds"`
DisputeGameFinalityDelaySeconds uint64 `cli:"dispute-game-finality-delay-seconds"`
SuperchainConfigProxy common.Address `cli:"superchain-config-proxy"`
ProtocolVersionsProxy common.Address `cli:"protocol-versions-proxy"`
UseInterop bool `cli:"use-interop"`
Logger log.Logger Logger log.Logger
ArtifactsLocator *artifacts.Locator
privateKeyECDSA *ecdsa.PrivateKey privateKeyECDSA *ecdsa.PrivateKey
PreimageOracle common.Address
} }
func (c *AsteriscConfig) Check() error { func (c *ImplementationsConfig) Check() error {
if c.L1RPCUrl == "" { if c.L1RPCUrl == "" {
return fmt.Errorf("l1RPCUrl must be specified") return errors.New("l1RPCUrl must be specified")
} }
if c.PrivateKey == "" { if c.PrivateKey == "" {
return fmt.Errorf("private key must be specified") return errors.New("private key must be specified")
} }
privECDSA, err := crypto.HexToECDSA(strings.TrimPrefix(c.PrivateKey, "0x")) privECDSA, err := crypto.HexToECDSA(strings.TrimPrefix(c.PrivateKey, "0x"))
...@@ -54,59 +61,67 @@ func (c *AsteriscConfig) Check() error { ...@@ -54,59 +61,67 @@ func (c *AsteriscConfig) Check() error {
c.privateKeyECDSA = privECDSA c.privateKeyECDSA = privECDSA
if c.Logger == nil { if c.Logger == nil {
return fmt.Errorf("logger must be specified") return errors.New("logger must be specified")
} }
if c.ArtifactsLocator == nil { if c.ArtifactsLocator == nil {
return fmt.Errorf("artifacts locator must be specified") return errors.New("artifacts locator must be specified")
} }
if c.L1ContractsRelease == "" {
if c.PreimageOracle == (common.Address{}) { return errors.New("L1 contracts release must be specified")
return fmt.Errorf("preimage oracle must be specified") }
if c.MIPSVersion != 1 && c.MIPSVersion != 2 {
return errors.New("MIPS version must be specified as either 1 or 2")
}
if c.WithdrawalDelaySeconds == 0 {
return errors.New("withdrawal delay in seconds must be specified")
}
if c.MinProposalSizeBytes == 0 {
return errors.New("preimage oracle minimum proposal size in bytes must be specified")
}
if c.ChallengePeriodSeconds == 0 {
return errors.New("preimage oracle challenge period in seconds must be specified")
}
if c.ProofMaturityDelaySeconds == 0 {
return errors.New("proof maturity delay in seconds must be specified")
}
if c.DisputeGameFinalityDelaySeconds == 0 {
return errors.New("dispute game finality delay in seconds must be specified")
}
if c.SuperchainConfigProxy == (common.Address{}) {
return errors.New("superchain config proxy must be specified")
}
if c.ProtocolVersionsProxy == (common.Address{}) {
return errors.New("protocol versions proxy must be specified")
} }
return nil return nil
} }
func AsteriscCLI(cliCtx *cli.Context) error { func ImplementationsCLI(cliCtx *cli.Context) error {
logCfg := oplog.ReadCLIConfig(cliCtx) logCfg := oplog.ReadCLIConfig(cliCtx)
l := oplog.NewLogger(oplog.AppOut(cliCtx), logCfg) l := oplog.NewLogger(oplog.AppOut(cliCtx), logCfg)
oplog.SetGlobalLogHandler(l.Handler()) oplog.SetGlobalLogHandler(l.Handler())
outfile := cliCtx.String(OutfileFlagName) var cfg ImplementationsConfig
l1RPCUrl := cliCtx.String(deployer.L1RPCURLFlagName) if err := cliutil.PopulateStruct(&cfg, cliCtx); err != nil {
privateKey := cliCtx.String(deployer.PrivateKeyFlagName) return fmt.Errorf("failed to populate config: %w", err)
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)
} }
preimageOracle := common.HexToAddress(cliCtx.String(PreimageOracleFlagName))
ctx := ctxinterrupt.WithCancelOnInterrupt(cliCtx.Context) ctx := ctxinterrupt.WithCancelOnInterrupt(cliCtx.Context)
outfile := cliCtx.String(OutfileFlagName)
dao, err := Asterisc(ctx, AsteriscConfig{ dio, err := Implementations(ctx, cfg)
L1RPCUrl: l1RPCUrl,
PrivateKey: privateKey,
Logger: l,
ArtifactsLocator: artifactsLocator,
PreimageOracle: preimageOracle,
})
if err != nil { if err != nil {
return fmt.Errorf("failed to deploy Asterisc: %w", err) return fmt.Errorf("failed to deploy implementations: %w", err)
} }
if err := jsonutil.WriteJSON(dio, ioutil.ToStdOutOrFileOrNoop(outfile, 0o755)); err != nil {
if err := jsonutil.WriteJSON(dao, ioutil.ToStdOutOrFileOrNoop(outfile, 0o755)); err != nil {
return fmt.Errorf("failed to write output: %w", err) return fmt.Errorf("failed to write output: %w", err)
} }
return nil return nil
} }
func Asterisc(ctx context.Context, cfg AsteriscConfig) (opcm.DeployAsteriscOutput, error) { func Implementations(ctx context.Context, cfg ImplementationsConfig) (opcm.DeployImplementationsOutput, error) {
var dao opcm.DeployAsteriscOutput var dio opcm.DeployImplementationsOutput
if err := cfg.Check(); err != nil { if err := cfg.Check(); err != nil {
return dao, fmt.Errorf("invalid config for Asterisc: %w", err) return dio, fmt.Errorf("invalid config for Implementations: %w", err)
} }
lgr := cfg.Logger lgr := cfg.Logger
...@@ -116,7 +131,7 @@ func Asterisc(ctx context.Context, cfg AsteriscConfig) (opcm.DeployAsteriscOutpu ...@@ -116,7 +131,7 @@ func Asterisc(ctx context.Context, cfg AsteriscConfig) (opcm.DeployAsteriscOutpu
artifactsFS, cleanup, err := artifacts.Download(ctx, cfg.ArtifactsLocator, progressor) artifactsFS, cleanup, err := artifacts.Download(ctx, cfg.ArtifactsLocator, progressor)
if err != nil { if err != nil {
return dao, fmt.Errorf("failed to download artifacts: %w", err) return dio, fmt.Errorf("failed to download artifacts: %w", err)
} }
defer func() { defer func() {
if err := cleanup(); err != nil { if err := cleanup(); err != nil {
...@@ -126,12 +141,12 @@ func Asterisc(ctx context.Context, cfg AsteriscConfig) (opcm.DeployAsteriscOutpu ...@@ -126,12 +141,12 @@ func Asterisc(ctx context.Context, cfg AsteriscConfig) (opcm.DeployAsteriscOutpu
l1Client, err := ethclient.Dial(cfg.L1RPCUrl) l1Client, err := ethclient.Dial(cfg.L1RPCUrl)
if err != nil { if err != nil {
return dao, fmt.Errorf("failed to connect to L1 RPC: %w", err) return dio, fmt.Errorf("failed to connect to L1 RPC: %w", err)
} }
chainID, err := l1Client.ChainID(ctx) chainID, err := l1Client.ChainID(ctx)
if err != nil { if err != nil {
return dao, fmt.Errorf("failed to get chain ID: %w", err) return dio, fmt.Errorf("failed to get chain ID: %w", err)
} }
signer := opcrypto.SignerFnFromBind(opcrypto.PrivateKeySignerFn(cfg.privateKeyECDSA, chainID)) signer := opcrypto.SignerFnFromBind(opcrypto.PrivateKeySignerFn(cfg.privateKeyECDSA, chainID))
...@@ -145,12 +160,12 @@ func Asterisc(ctx context.Context, cfg AsteriscConfig) (opcm.DeployAsteriscOutpu ...@@ -145,12 +160,12 @@ func Asterisc(ctx context.Context, cfg AsteriscConfig) (opcm.DeployAsteriscOutpu
From: chainDeployer, From: chainDeployer,
}) })
if err != nil { if err != nil {
return dao, fmt.Errorf("failed to create broadcaster: %w", err) return dio, fmt.Errorf("failed to create broadcaster: %w", err)
} }
l1RPC, err := rpc.Dial(cfg.L1RPCUrl) l1RPC, err := rpc.Dial(cfg.L1RPCUrl)
if err != nil { if err != nil {
return dao, fmt.Errorf("failed to connect to L1 RPC: %w", err) return dio, fmt.Errorf("failed to connect to L1 RPC: %w", err)
} }
l1Host, err := env.DefaultForkedScriptHost( l1Host, err := env.DefaultForkedScriptHost(
...@@ -162,23 +177,31 @@ func Asterisc(ctx context.Context, cfg AsteriscConfig) (opcm.DeployAsteriscOutpu ...@@ -162,23 +177,31 @@ func Asterisc(ctx context.Context, cfg AsteriscConfig) (opcm.DeployAsteriscOutpu
l1RPC, l1RPC,
) )
if err != nil { if err != nil {
return dao, fmt.Errorf("failed to create script host: %w", err) return dio, fmt.Errorf("failed to create script host: %w", err)
} }
dao, err = opcm.DeployAsterisc( if dio, err = opcm.DeployImplementations(
l1Host, l1Host,
opcm.DeployAsteriscInput{ opcm.DeployImplementationsInput{
PreimageOracle: cfg.PreimageOracle, WithdrawalDelaySeconds: new(big.Int).SetUint64(cfg.WithdrawalDelaySeconds),
MinProposalSizeBytes: new(big.Int).SetUint64(cfg.MinProposalSizeBytes),
ChallengePeriodSeconds: new(big.Int).SetUint64(cfg.ChallengePeriodSeconds),
ProofMaturityDelaySeconds: new(big.Int).SetUint64(cfg.ProofMaturityDelaySeconds),
DisputeGameFinalityDelaySeconds: new(big.Int).SetUint64(cfg.DisputeGameFinalityDelaySeconds),
MipsVersion: new(big.Int).SetUint64(uint64(cfg.MIPSVersion)),
L1ContractsRelease: cfg.L1ContractsRelease,
SuperchainConfigProxy: cfg.SuperchainConfigProxy,
ProtocolVersionsProxy: cfg.ProtocolVersionsProxy,
UseInterop: cfg.UseInterop,
}, },
) ); err != nil {
if err != nil { return dio, fmt.Errorf("error deploying implementations: %w", err)
return dao, fmt.Errorf("error deploying asterisc VM: %w", err)
} }
if _, err := bcaster.Broadcast(ctx); err != nil { if _, err := bcaster.Broadcast(ctx); err != nil {
return dao, fmt.Errorf("failed to broadcast: %w", err) return dio, fmt.Errorf("failed to broadcast: %w", err)
} }
lgr.Info("deployed asterisc VM") lgr.Info("deployed implementations")
return dao, nil return dio, nil
} }
...@@ -8,39 +8,29 @@ import ( ...@@ -8,39 +8,29 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/testutil"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/retryproxy" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/retryproxy"
"github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum-optimism/optimism/op-service/testutils/anvil" "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/ethclient"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var networks = []string{"mainnet", "sepolia"} func TestImplementations(t *testing.T) {
var versions = []string{"v1.8.0-rc.4", "v1.6.0"}
func TestOPCMLiveChain(t *testing.T) {
for _, network := range networks { for _, network := range networks {
for _, version := range versions { t.Run(network, func(t *testing.T) {
t.Run(network+"-"+version, func(t *testing.T) {
if version == "v1.8.0-rc.4" && network == "mainnet" {
t.Skip("v1.8.0-rc.4 not supported on mainnet yet")
}
if version == "v1.6.0" {
t.Skip("v1.6.0 not supported")
}
envVar := strings.ToUpper(network) + "_RPC_URL" envVar := strings.ToUpper(network) + "_RPC_URL"
rpcURL := os.Getenv(envVar) rpcURL := os.Getenv(envVar)
require.NotEmpty(t, rpcURL, "must specify RPC url via %s env var", envVar) require.NotEmpty(t, rpcURL, "must specify RPC url via %s env var", envVar)
testOPCMLiveChain(t, "op-contracts/"+version, rpcURL) testImplementations(t, rpcURL)
}) })
} }
}
} }
func testOPCMLiveChain(t *testing.T, version string, forkRPCURL string) { func testImplementations(t *testing.T, forkRPCURL string) {
t.Parallel() t.Parallel()
if forkRPCURL == "" { if forkRPCURL == "" {
...@@ -69,18 +59,40 @@ func testOPCMLiveChain(t *testing.T, version string, forkRPCURL string) { ...@@ -69,18 +59,40 @@ func testOPCMLiveChain(t *testing.T, version string, forkRPCURL string) {
require.NoError(t, runner.Stop()) require.NoError(t, runner.Stop())
}) })
out, err := OPCM(ctx, OPCMConfig{ client, err := ethclient.Dial(runner.RPCUrl())
require.NoError(t, err)
chainID, err := client.ChainID(ctx)
require.NoError(t, err)
superchain, err := standard.SuperchainFor(chainID.Uint64())
require.NoError(t, err)
loc, _ := testutil.LocalArtifacts(t)
deploy := func() opcm.DeployImplementationsOutput {
out, err := Implementations(ctx, ImplementationsConfig{
L1RPCUrl: runner.RPCUrl(), L1RPCUrl: runner.RPCUrl(),
PrivateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", PrivateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
Release: version, ArtifactsLocator: loc,
Logger: lgr, Logger: lgr,
L1ContractsRelease: "dev",
WithdrawalDelaySeconds: standard.WithdrawalDelaySeconds,
MinProposalSizeBytes: standard.MinProposalSizeBytes,
ChallengePeriodSeconds: standard.ChallengePeriodSeconds,
ProofMaturityDelaySeconds: standard.ProofMaturityDelaySeconds,
DisputeGameFinalityDelaySeconds: standard.DisputeGameFinalityDelaySeconds,
MIPSVersion: 1,
SuperchainConfigProxy: common.Address(*superchain.Config.SuperchainConfigAddr),
ProtocolVersionsProxy: common.Address(*superchain.Config.ProtocolVersionsAddr),
UseInterop: false,
}) })
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, out.Opcm) return out
}
client, err := ethclient.Dial(runner.RPCUrl()) // Assert that addresses stay the same between runs
require.NoError(t, err) deployment1 := deploy()
code, err := client.CodeAt(ctx, out.Opcm, nil) deployment2 := deploy()
require.NoError(t, err) require.Equal(t, deployment1, deployment2)
require.NotEmpty(t, code)
} }
package bootstrap
import (
"context"
"crypto/ecdsa"
"fmt"
"strings"
"github.com/ethereum/go-ethereum/rpc"
artifacts2 "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/env"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
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/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/urfave/cli/v2"
)
type MIPSConfig struct {
L1RPCUrl string
PrivateKey string
Logger log.Logger
ArtifactsLocator *artifacts2.Locator
privateKeyECDSA *ecdsa.PrivateKey
PreimageOracle common.Address
MipsVersion uint64
}
func (c *MIPSConfig) 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.PreimageOracle == (common.Address{}) {
return fmt.Errorf("preimage oracle must be specified")
}
if c.MipsVersion == 0 {
return fmt.Errorf("mips version must be specified")
}
if c.MipsVersion != 1 && c.MipsVersion != 2 {
return fmt.Errorf("mips version must be either 1 or 2")
}
return nil
}
func MIPSCLI(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)
outfile := cliCtx.String(OutfileFlagName)
artifactsURLStr := cliCtx.String(ArtifactsLocatorFlagName)
artifactsLocator := new(artifacts2.Locator)
if err := artifactsLocator.UnmarshalText([]byte(artifactsURLStr)); err != nil {
return fmt.Errorf("failed to parse artifacts URL: %w", err)
}
mipsVersion := cliCtx.Uint64(MIPSVersionFlagName)
preimageOracle := common.HexToAddress(cliCtx.String(PreimageOracleFlagName))
ctx := ctxinterrupt.WithCancelOnInterrupt(cliCtx.Context)
dmo, err := MIPS(ctx, MIPSConfig{
L1RPCUrl: l1RPCUrl,
PrivateKey: privateKey,
Logger: l,
ArtifactsLocator: artifactsLocator,
MipsVersion: mipsVersion,
PreimageOracle: preimageOracle,
})
if err != nil {
return fmt.Errorf("failed to deploy MIPS: %w", err)
}
if err := jsonutil.WriteJSON(dmo, ioutil.ToStdOutOrFileOrNoop(outfile, 0o755)); err != nil {
return fmt.Errorf("failed to write output: %w", err)
}
return nil
}
func MIPS(ctx context.Context, cfg MIPSConfig) (opcm.DeployMIPSOutput, error) {
var dmo opcm.DeployMIPSOutput
if err := cfg.Check(); err != nil {
return dmo, fmt.Errorf("invalid config for MIPS: %w", err)
}
lgr := cfg.Logger
progressor := func(curr, total int64) {
lgr.Info("artifacts download progress", "current", curr, "total", total)
}
artifactsFS, cleanup, err := artifacts2.Download(ctx, cfg.ArtifactsLocator, progressor)
if err != nil {
return dmo, fmt.Errorf("failed to download artifacts: %w", err)
}
defer func() {
if err := cleanup(); err != nil {
lgr.Warn("failed to clean up artifacts", "err", err)
}
}()
l1RPC, err := rpc.Dial(cfg.L1RPCUrl)
if err != nil {
return dmo, fmt.Errorf("failed to connect to L1 RPC: %w", err)
}
l1Client := ethclient.NewClient(l1RPC)
chainID, err := l1Client.ChainID(ctx)
if err != nil {
return dmo, 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 dmo, fmt.Errorf("failed to create broadcaster: %w", err)
}
host, err := env.DefaultForkedScriptHost(
ctx,
bcaster,
lgr,
chainDeployer,
artifactsFS,
l1RPC,
)
if err != nil {
return dmo, fmt.Errorf("failed to create script host: %w", err)
}
var release string
if cfg.ArtifactsLocator.IsTag() {
release = cfg.ArtifactsLocator.Tag
} else {
release = "dev"
}
lgr.Info("deploying dispute game", "release", release)
dmo, err = opcm.DeployMIPS(
host,
opcm.DeployMIPSInput{
MipsVersion: cfg.MipsVersion,
PreimageOracle: cfg.PreimageOracle,
},
)
if err != nil {
return dmo, fmt.Errorf("error deploying dispute game: %w", err)
}
if _, err := bcaster.Broadcast(ctx); err != nil {
return dmo, fmt.Errorf("failed to broadcast: %w", err)
}
lgr.Info("deployed dispute game")
return dmo, nil
}
package bootstrap
import (
"context"
"crypto/ecdsa"
"fmt"
"strings"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/env"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
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/urfave/cli/v2"
)
type OPCMConfig struct {
L1RPCUrl string
PrivateKey string
Release string
Logger log.Logger
privateKeyECDSA *ecdsa.PrivateKey
}
func (c *OPCMConfig) Check() error {
if c.L1RPCUrl == "" {
return fmt.Errorf("l1RPCUrl must be specified")
}
if c.PrivateKey == "" {
return fmt.Errorf("private key must be specified")
}
if c.Release == "" {
return fmt.Errorf("release 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")
}
return nil
}
func OPCMCLI(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)
outfile := cliCtx.String(OutfileFlagName)
release := cliCtx.String(ReleaseFlagName)
ctx := ctxinterrupt.WithCancelOnInterrupt(cliCtx.Context)
out, err := OPCM(ctx, OPCMConfig{
L1RPCUrl: l1RPCUrl,
PrivateKey: privateKey,
Release: release,
Logger: l,
})
if err != nil {
return fmt.Errorf("failed to deploy OPCM: %w", err)
}
if err := jsonutil.WriteJSON(out, ioutil.ToStdOutOrFileOrNoop(outfile, 0o755)); err != nil {
return fmt.Errorf("failed to write output: %w", err)
}
return nil
}
func OPCM(ctx context.Context, cfg OPCMConfig) (opcm.DeployOPCMOutput, error) {
var out opcm.DeployOPCMOutput
if err := cfg.Check(); err != nil {
return out, fmt.Errorf("invalid config for OPCM: %w", err)
}
lgr := cfg.Logger
progressor := func(curr, total int64) {
lgr.Info("artifacts download progress", "current", curr, "total", total)
}
l1RPC, err := rpc.Dial(cfg.L1RPCUrl)
if err != nil {
return out, fmt.Errorf("failed to connect to L1 RPC: %w", err)
}
l1Client := ethclient.NewClient(l1RPC)
chainID, err := l1Client.ChainID(ctx)
if err != nil {
return out, fmt.Errorf("failed to get chain ID: %w", err)
}
chainIDU64 := chainID.Uint64()
loc, err := artifacts.NewLocatorFromTag(cfg.Release)
if err != nil {
return out, fmt.Errorf("failed to create artifacts locator: %w", err)
}
artifactsFS, cleanup, err := artifacts.Download(ctx, loc, progressor)
if err != nil {
return out, fmt.Errorf("failed to download artifacts: %w", err)
}
defer func() {
if err := cleanup(); err != nil {
lgr.Warn("failed to clean up artifacts", "err", 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 out, fmt.Errorf("failed to create broadcaster: %w", err)
}
host, err := env.DefaultForkedScriptHost(
ctx,
bcaster,
lgr,
chainDeployer,
artifactsFS,
l1RPC,
)
if err != nil {
return out, fmt.Errorf("failed to create script host: %w", err)
}
lgr.Info("deploying OPCM", "l1ContractsRelease", cfg.Release)
input, err := DeployOPCMInputForChain(cfg.Release, chainIDU64)
if err != nil {
return out, fmt.Errorf("error creating OPCM input: %w", err)
}
out, err = opcm.DeployOPCM(
host,
input,
)
if err != nil {
return out, fmt.Errorf("error deploying implementations: %w", err)
}
if _, err := bcaster.Broadcast(ctx); err != nil {
return out, fmt.Errorf("failed to broadcast: %w", err)
}
lgr.Info("deployed OPCM")
return out, nil
}
func DeployOPCMInputForChain(release string, chainID uint64) (opcm.DeployOPCMInput, error) {
superchain, err := standard.SuperchainFor(chainID)
if err != nil {
return opcm.DeployOPCMInput{}, fmt.Errorf("error getting superchain config: %w", err)
}
l1VersionsData, err := standard.L1VersionsFor(chainID)
if err != nil {
return opcm.DeployOPCMInput{}, fmt.Errorf("error getting L1 versions: %w", err)
}
releases, ok := l1VersionsData[release]
if !ok {
return opcm.DeployOPCMInput{}, fmt.Errorf("release not found: %s", release)
}
blueprints, err := standard.OPCMBlueprintsFor(chainID, release)
if err != nil {
return opcm.DeployOPCMInput{}, fmt.Errorf("error getting OPCM blueprints: %w", err)
}
return opcm.DeployOPCMInput{
SuperchainConfig: common.Address(*superchain.Config.SuperchainConfigAddr),
ProtocolVersions: common.Address(*superchain.Config.ProtocolVersionsAddr),
L1ContractsRelease: strings.TrimPrefix(release, "op-contracts/"),
AddressManagerBlueprint: blueprints.AddressManager,
ProxyBlueprint: blueprints.Proxy,
ProxyAdminBlueprint: blueprints.ProxyAdmin,
L1ChugSplashProxyBlueprint: blueprints.L1ChugSplashProxy,
ResolvedDelegateProxyBlueprint: blueprints.ResolvedDelegateProxy,
AnchorStateRegistryBlueprint: blueprints.AnchorStateRegistry,
PermissionedDisputeGame1Blueprint: blueprints.PermissionedDisputeGame1,
PermissionedDisputeGame2Blueprint: blueprints.PermissionedDisputeGame2,
L1ERC721BridgeImpl: releases.L1ERC721Bridge.ImplementationAddress,
OptimismPortalImpl: releases.OptimismPortal.ImplementationAddress,
SystemConfigImpl: releases.SystemConfig.ImplementationAddress,
OptimismMintableERC20FactoryImpl: releases.OptimismMintableERC20Factory.ImplementationAddress,
L1CrossDomainMessengerImpl: releases.L1CrossDomainMessenger.ImplementationAddress,
L1StandardBridgeImpl: releases.L1StandardBridge.ImplementationAddress,
DisputeGameFactoryImpl: releases.DisputeGameFactory.ImplementationAddress,
DelayedWETHImpl: releases.DelayedWETH.ImplementationAddress,
MipsImpl: releases.MIPS.Address,
}, nil
}
...@@ -18,6 +18,10 @@ import ( ...@@ -18,6 +18,10 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var networks = []string{"mainnet", "sepolia"}
var versions = []string{"v1.6.0", "v1.8.0-rc.4"}
func TestSuperchain(t *testing.T) { func TestSuperchain(t *testing.T) {
for _, network := range networks { for _, network := range networks {
for _, version := range versions { for _, version := range versions {
......
...@@ -10,7 +10,6 @@ import ( ...@@ -10,7 +10,6 @@ import (
) )
type DeployImplementationsInput struct { type DeployImplementationsInput struct {
Salt common.Hash
WithdrawalDelaySeconds *big.Int WithdrawalDelaySeconds *big.Int
MinProposalSizeBytes *big.Int MinProposalSizeBytes *big.Int
ChallengePeriodSeconds *big.Int ChallengePeriodSeconds *big.Int
...@@ -22,8 +21,6 @@ type DeployImplementationsInput struct { ...@@ -22,8 +21,6 @@ type DeployImplementationsInput struct {
SuperchainConfigProxy common.Address SuperchainConfigProxy common.Address
ProtocolVersionsProxy common.Address ProtocolVersionsProxy common.Address
UseInterop bool // if true, deploy Interop implementations UseInterop bool // if true, deploy Interop implementations
StandardVersionsToml string // contents of 'standard-versions-mainnet.toml' or 'standard-versions-sepolia.toml' file
} }
func (input *DeployImplementationsInput) InputSet() bool { func (input *DeployImplementationsInput) InputSet() bool {
......
...@@ -21,21 +21,14 @@ func DeployImplementations(env *Env, intent *state.Intent, st *state.State) erro ...@@ -21,21 +21,14 @@ func DeployImplementations(env *Env, intent *state.Intent, st *state.State) erro
lgr.Info("deploying implementations") lgr.Info("deploying implementations")
var standardVersionsTOML string
var contractsRelease string var contractsRelease string
var err error var err error
if intent.L1ContractsLocator.IsTag() { if intent.L1ContractsLocator.IsTag() {
standardVersionsTOML, err = standard.L1VersionsDataFor(intent.L1ChainID)
if err == nil {
contractsRelease = intent.L1ContractsLocator.Tag contractsRelease = intent.L1ContractsLocator.Tag
} else { } else {
contractsRelease = "dev" contractsRelease = "dev"
} }
} else {
contractsRelease = "dev"
}
proofParams, err := jsonutil.MergeJSON( proofParams, err := jsonutil.MergeJSON(
state.SuperchainProofParams{ state.SuperchainProofParams{
WithdrawalDelaySeconds: standard.WithdrawalDelaySeconds, WithdrawalDelaySeconds: standard.WithdrawalDelaySeconds,
...@@ -54,7 +47,6 @@ func DeployImplementations(env *Env, intent *state.Intent, st *state.State) erro ...@@ -54,7 +47,6 @@ func DeployImplementations(env *Env, intent *state.Intent, st *state.State) erro
dio, err := opcm.DeployImplementations( dio, err := opcm.DeployImplementations(
env.L1ScriptHost, env.L1ScriptHost,
opcm.DeployImplementationsInput{ opcm.DeployImplementationsInput{
Salt: st.Create2Salt,
WithdrawalDelaySeconds: new(big.Int).SetUint64(proofParams.WithdrawalDelaySeconds), WithdrawalDelaySeconds: new(big.Int).SetUint64(proofParams.WithdrawalDelaySeconds),
MinProposalSizeBytes: new(big.Int).SetUint64(proofParams.MinProposalSizeBytes), MinProposalSizeBytes: new(big.Int).SetUint64(proofParams.MinProposalSizeBytes),
ChallengePeriodSeconds: new(big.Int).SetUint64(proofParams.ChallengePeriodSeconds), ChallengePeriodSeconds: new(big.Int).SetUint64(proofParams.ChallengePeriodSeconds),
...@@ -64,7 +56,6 @@ func DeployImplementations(env *Env, intent *state.Intent, st *state.State) erro ...@@ -64,7 +56,6 @@ func DeployImplementations(env *Env, intent *state.Intent, st *state.State) erro
L1ContractsRelease: contractsRelease, L1ContractsRelease: contractsRelease,
SuperchainConfigProxy: st.SuperchainDeployment.SuperchainConfigProxyAddress, SuperchainConfigProxy: st.SuperchainDeployment.SuperchainConfigProxyAddress,
ProtocolVersionsProxy: st.SuperchainDeployment.ProtocolVersionsProxyAddress, ProtocolVersionsProxy: st.SuperchainDeployment.ProtocolVersionsProxyAddress,
StandardVersionsToml: standardVersionsTOML,
UseInterop: intent.UseInterop, UseInterop: intent.UseInterop,
}, },
) )
......
package cliutil
import (
"encoding"
"fmt"
"reflect"
"github.com/ethereum/go-ethereum/common"
"github.com/urfave/cli/v2"
)
// PopulateStruct populates a struct with values from CLI context based on `cli` tags.
func PopulateStruct(cfg any, ctx *cli.Context) error {
// Get reflected value of the config struct
v := reflect.ValueOf(cfg)
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
return fmt.Errorf("config must be a pointer to struct")
}
v = v.Elem()
t := v.Type()
// Iterate over struct fields
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldValue := v.Field(i)
// Get CLI tag
cliTag := field.Tag.Get("cli")
if cliTag == "" {
continue
}
// Skip if field is not settable
if !fieldValue.CanSet() {
continue
}
// Handle different types
if err := setFieldValue(fieldValue, field.Type, ctx, cliTag); err != nil {
return fmt.Errorf("failed to set field %s: %w", field.Name, err)
}
}
return nil
}
// setFieldValue sets the appropriate value based on the field type
func setFieldValue(fieldValue reflect.Value, fieldType reflect.Type, ctx *cli.Context, flag string) error {
switch fieldType.Kind() {
case reflect.String:
fieldValue.SetString(ctx.String(flag))
case reflect.Bool:
fieldValue.SetBool(ctx.Bool(flag))
case reflect.Int, reflect.Int64:
fieldValue.SetInt(int64(ctx.Int(flag)))
case reflect.Uint64:
fieldValue.SetUint(ctx.Uint64(flag))
case reflect.Ptr:
if !ctx.IsSet(flag) {
return nil // Skip if flag is not set
}
// Handle pointer types
return handlePointerType(fieldValue, fieldType, ctx, flag)
default:
// Handle special types
return handleSpecialTypes(fieldValue, fieldType, ctx, flag)
}
return nil
}
// handlePointerType handles pointer type fields
func handlePointerType(fieldValue reflect.Value, fieldType reflect.Type, ctx *cli.Context, flag string) error {
// Create a new instance of the pointed-to type
elem := reflect.New(fieldType.Elem())
// If it implements TextUnmarshaler, use that
if unmarshaler, ok := elem.Interface().(encoding.TextUnmarshaler); ok {
if err := unmarshaler.UnmarshalText([]byte(ctx.String(flag))); err != nil {
return err
}
fieldValue.Set(elem)
return nil
}
// Handle other pointer types as needed
return fmt.Errorf("unsupported pointer type: %v", fieldType)
}
// handleSpecialTypes handles non-primitive types that need special handling
func handleSpecialTypes(fieldValue reflect.Value, fieldType reflect.Type, ctx *cli.Context, flag string) error {
// Handle common.Address
if fieldType == reflect.TypeOf(common.Address{}) {
if !ctx.IsSet(flag) {
return nil
}
addrStr := ctx.String(flag)
if !common.IsHexAddress(addrStr) {
return fmt.Errorf("invalid address: %s", addrStr)
}
addr := common.HexToAddress(addrStr)
fieldValue.Set(reflect.ValueOf(addr))
return nil
}
// If type implements TextUnmarshaler
if unmarshaler, ok := fieldValue.Interface().(encoding.TextUnmarshaler); ok {
return unmarshaler.UnmarshalText([]byte(ctx.String(flag)))
}
return fmt.Errorf("unsupported type: %v", fieldType)
}
package cliutil
import (
"fmt"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
)
type textUnmarshalerThing struct {
text string
}
func (t *textUnmarshalerThing) UnmarshalText(text []byte) error {
t.text = string(text)
return nil
}
func TestPopulateStruct(t *testing.T) {
type testStruct struct {
Str string `cli:"str"`
Bool bool `cli:"bool"`
Int int `cli:"int"`
Int64 int64 `cli:"int64"`
Uint64 uint64 `cli:"uint64"`
Address common.Address `cli:"address"`
TextUnmarshaler *textUnmarshalerThing `cli:"text-unmarshaler"`
NotTagged string
}
tests := []struct {
name string
args []string
exp testStruct
expErr string
}{
{
name: "all flags",
args: []string{
"--str=test",
"--bool",
"--int=1",
"--int64=2",
"--uint64=3",
fmt.Sprintf("--address=%s", common.HexToAddress("0x42")),
"--text-unmarshaler=hello",
},
exp: testStruct{
Str: "test",
Bool: true,
Int: 1,
Int64: 2,
Uint64: 3,
Address: common.HexToAddress("0x42"),
TextUnmarshaler: &textUnmarshalerThing{
text: "hello",
},
},
},
{
name: "no flags",
args: []string{},
exp: testStruct{},
},
{
name: "invalid address flag",
args: []string{
"--address=not-an-address",
},
expErr: "invalid address",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app := &cli.App{
Name: "test",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "str",
},
&cli.BoolFlag{
Name: "bool",
},
&cli.IntFlag{
Name: "int",
},
&cli.Int64Flag{
Name: "int64",
},
&cli.Uint64Flag{
Name: "uint64",
},
&cli.StringFlag{
Name: "address",
},
&cli.StringFlag{
Name: "text-unmarshaler",
},
},
Action: func(cliCtx *cli.Context) error {
ts := testStruct{}
if tt.expErr == "" {
require.NoError(t, PopulateStruct(&ts, cliCtx))
require.EqualValues(t, tt.exp, ts)
} else {
require.ErrorContains(t, PopulateStruct(&ts, cliCtx), tt.expErr)
}
return nil
},
}
require.NoError(t, app.Run(append([]string{"program-goes-here"}, tt.args...)))
})
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment