Commit e9b80ee1 authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

Add op-deployer proof-of-concept (#11804)

This PR adds a proof-of-concept for `op-deployer`, a CLI tool that allows declarative management of live OP Stack chains. This POC supports initializing the declarative chain config (called an "intent") and deploying the Superchain smart contracts using the OP Stack Manager.

An example intent for a Sepolia chain looks like this:

```toml
l1ChainID = 11155111
useFaultProofs = true
useAltDA = false
fundDevAccounts = true
contractArtifactsURL = "file:///Users/matthewslipper/dev/optimism/packages/contracts-bedrock/forge-artifacts"

[superchainRoles]
  proxyAdminOwner = "0xb9cdf788704088a4c0191d045c151fcbe2db14a4"
  protocolVersionsOwner = "0xb910764be39c84d572ff17713c615b5bfd7df650"
  guardian = "0x8c7e4a51acb17719d225bd17598b8a94b46c8767"
```

When deployed, it produces a state file that looks like this:

```json
{
  "version": 1,
  "appliedIntent": {
    "l1ChainID": 11155111,
    "superchainRoles": {
      "proxyAdminOwner": "0xb9cdf788704088a4c0191d045c151fcbe2db14a4",
      "protocolVersionsOwner": "0xb910764be39c84d572ff17713c615b5bfd7df650",
      "guardian": "0x8c7e4a51acb17719d225bd17598b8a94b46c8767"
    },
    "useFaultProofs": true,
    "useAltDA": false,
    "fundDevAccounts": true,
    "contractArtifactsURL": "file:///Users/matthewslipper/dev/optimism/packages/contracts-bedrock/forge-artifacts",
    "chains": null
  },
  "superchainDeployment": {
    "proxyAdminAddress": "0x54a6088c04a7782e69b5031579a1973a9e3c1a8c",
    "superchainConfigProxyAddress": "0xc969afc4799a9350f9f05b60748bc62f2829b03a",
    "superchainConfigImplAddress": "0x08426b74350e7cba5b52be4909c542d28b6b3962",
    "protocolVersionsProxyAddress": "0x212a023892803c7570eb317c77672c8391bf3dde",
    "protocolVersionsImplAddress": "0x2633ac74edb7ae1f1b5656e042285015f9ee477d"
  }
}
```

To use `op-deployer`, run `op-deployer init --dev --l1-chain-id <chain-id>`. This will initialize a deployment intent using the development keys in the repo. Then, run `op-deployer apply --l1-rpc-url <l1-rpc> --private-key <deployer-private-key>` to apply the deployment.

- The contracts deployment is performed by the local Go/Forge tooling.
- Upgrades of the contracts (i.e. modifying them after deploying the contracts afresh) is not currently supported. This will be supported in the future.
- The rest of the pipeline (i.e., deploying L2s and generating genesis files) is not included in this PR to keep it smaller and allow us to get buy-in on the fundamental concepts behind `op-deployer` before further implementation.
parent fe4890f6
bin bin
intent.toml
state.json
\ No newline at end of file
...@@ -12,10 +12,13 @@ receipt-reference-builder: ...@@ -12,10 +12,13 @@ receipt-reference-builder:
test: test:
go test ./... go test ./...
op-deployer:
go build -o ./bin/op-deployer ./cmd/op-deployer/main.go
fuzz: fuzz:
go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzEncodeDecodeWithdrawal ./crossdomain go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzEncodeDecodeWithdrawal ./crossdomain
go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzEncodeDecodeLegacyWithdrawal ./crossdomain go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzEncodeDecodeLegacyWithdrawal ./crossdomain
go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzAliasing ./crossdomain go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzAliasing ./crossdomain
go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzVersionedNonce ./crossdomain go test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzVersionedNonce ./crossdomain
.PHONY: test fuzz .PHONY: test fuzz op-deployer
\ No newline at end of file
package main
import (
"fmt"
"os"
"github.com/ethereum-optimism/optimism/op-chain-ops/deployer"
"github.com/ethereum-optimism/optimism/op-service/cliapp"
"github.com/urfave/cli/v2"
)
func main() {
app := cli.NewApp()
app.Name = "op-deployer"
app.Usage = "Tool to configure and deploy OP Chains."
app.Flags = cliapp.ProtectFlags(deployer.GlobalFlags)
app.Commands = []*cli.Command{
{
Name: "init",
Usage: "initializes a chain intent and state file",
Flags: cliapp.ProtectFlags(deployer.InitFlags),
Action: deployer.InitCLI(),
},
{
Name: "apply",
Usage: "applies a chain intent to the chain",
Flags: cliapp.ProtectFlags(deployer.ApplyFlags),
Action: deployer.ApplyCLI(),
},
}
app.Writer = os.Stdout
app.ErrWriter = os.Stderr
err := app.Run(os.Args)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Application failed: %v\n", err)
os.Exit(1)
}
}
package deployer
import (
"context"
"crypto/ecdsa"
"fmt"
"strings"
"github.com/ethereum-optimism/optimism/op-chain-ops/deployer/pipeline"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
"github.com/ethereum-optimism/optimism/op-service/ctxinterrupt"
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 ApplyConfig struct {
L1RPCUrl string
Workdir string
PrivateKey string
Logger log.Logger
privateKeyECDSA *ecdsa.PrivateKey
}
func (a *ApplyConfig) Check() error {
if a.L1RPCUrl == "" {
return fmt.Errorf("l1RPCUrl must be specified")
}
if a.Workdir == "" {
return fmt.Errorf("workdir must be specified")
}
if a.PrivateKey == "" {
return fmt.Errorf("private key must be specified")
}
privECDSA, err := crypto.HexToECDSA(strings.TrimPrefix(a.PrivateKey, "0x"))
if err != nil {
return fmt.Errorf("failed to parse private key: %w", err)
}
a.privateKeyECDSA = privECDSA
if a.Logger == nil {
return fmt.Errorf("logger must be specified")
}
return nil
}
func ApplyCLI() func(cliCtx *cli.Context) error {
return func(cliCtx *cli.Context) error {
logCfg := oplog.ReadCLIConfig(cliCtx)
l := oplog.NewLogger(oplog.AppOut(cliCtx), logCfg)
oplog.SetGlobalLogHandler(l.Handler())
l1RPCUrl := cliCtx.String(L1RPCURLFlagName)
workdir := cliCtx.String(WorkdirFlagName)
privateKey := cliCtx.String(PrivateKeyFlagName)
ctx := ctxinterrupt.WithCancelOnInterrupt(cliCtx.Context)
return Apply(ctx, ApplyConfig{
L1RPCUrl: l1RPCUrl,
Workdir: workdir,
PrivateKey: privateKey,
Logger: l,
})
}
}
func Apply(ctx context.Context, cfg ApplyConfig) error {
if err := cfg.Check(); err != nil {
return fmt.Errorf("invalid config for apply: %w", err)
}
l1Client, err := ethclient.Dial(cfg.L1RPCUrl)
if err != nil {
return fmt.Errorf("failed to connect to L1 RPC: %w", err)
}
chainID, err := l1Client.ChainID(ctx)
if err != nil {
return fmt.Errorf("failed to get chain ID: %w", err)
}
signer := opcrypto.SignerFnFromBind(opcrypto.PrivateKeySignerFn(cfg.privateKeyECDSA, chainID))
deployer := crypto.PubkeyToAddress(cfg.privateKeyECDSA.PublicKey)
env := &pipeline.Env{
Workdir: cfg.Workdir,
L1RPCUrl: cfg.L1RPCUrl,
L1Client: l1Client,
Logger: cfg.Logger,
Signer: signer,
Deployer: deployer,
}
intent, err := env.ReadIntent()
if err != nil {
return err
}
if err := intent.Check(); err != nil {
return fmt.Errorf("invalid intent: %w", err)
}
st, err := env.ReadState()
if err != nil {
return err
}
pline := []struct {
name string
stage pipeline.Stage
}{
{"init", pipeline.Init},
{"deploy-superchain", pipeline.DeploySuperchain},
}
for _, stage := range pline {
if err := stage.stage(ctx, env, intent, st); err != nil {
return fmt.Errorf("error in pipeline stage: %w", err)
}
}
st.AppliedIntent = intent
if err := env.WriteState(st); err != nil {
return err
}
return nil
}
package broadcaster
import (
"context"
"github.com/ethereum-optimism/optimism/op-chain-ops/script"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
type Broadcaster interface {
Broadcast(ctx context.Context) ([]BroadcastResult, error)
Hook(bcast script.Broadcast)
}
type BroadcastResult struct {
Broadcast script.Broadcast `json:"broadcast"`
TxHash common.Hash `json:"txHash"`
Receipt *types.Receipt `json:"receipt"`
Err error `json:"-"`
}
package broadcaster
import (
"context"
"fmt"
"math/big"
"time"
"github.com/ethereum-optimism/optimism/op-chain-ops/script"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum-optimism/optimism/op-service/txmgr/metrics"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/hashicorp/go-multierror"
"github.com/holiman/uint256"
)
const (
GasPadFactor = 1.5
)
type KeyedBroadcaster struct {
lgr log.Logger
mgr txmgr.TxManager
bcasts []script.Broadcast
}
type KeyedBroadcasterOpts struct {
Logger log.Logger
ChainID *big.Int
Client *ethclient.Client
Signer opcrypto.SignerFn
From common.Address
}
func NewKeyedBroadcaster(cfg KeyedBroadcasterOpts) (*KeyedBroadcaster, error) {
mgrCfg := &txmgr.Config{
Backend: cfg.Client,
ChainID: cfg.ChainID,
TxSendTimeout: 5 * time.Minute,
TxNotInMempoolTimeout: time.Minute,
NetworkTimeout: 10 * time.Second,
ReceiptQueryInterval: time.Second,
NumConfirmations: 1,
SafeAbortNonceTooLowCount: 3,
Signer: cfg.Signer,
From: cfg.From,
}
minTipCap, err := eth.GweiToWei(1.0)
if err != nil {
panic(err)
}
minBaseFee, err := eth.GweiToWei(1.0)
if err != nil {
panic(err)
}
mgrCfg.ResubmissionTimeout.Store(int64(48 * time.Second))
mgrCfg.FeeLimitMultiplier.Store(5)
mgrCfg.FeeLimitThreshold.Store(big.NewInt(100))
mgrCfg.MinTipCap.Store(minTipCap)
mgrCfg.MinTipCap.Store(minBaseFee)
mgr, err := txmgr.NewSimpleTxManagerFromConfig(
"transactor",
log.NewLogger(log.DiscardHandler()),
&metrics.NoopTxMetrics{},
mgrCfg,
)
if err != nil {
return nil, fmt.Errorf("failed to create tx manager: %w", err)
}
return &KeyedBroadcaster{
lgr: cfg.Logger,
mgr: mgr,
}, nil
}
func (t *KeyedBroadcaster) Hook(bcast script.Broadcast) {
t.bcasts = append(t.bcasts, bcast)
}
func (t *KeyedBroadcaster) Broadcast(ctx context.Context) ([]BroadcastResult, error) {
results := make([]BroadcastResult, len(t.bcasts))
futures := make([]<-chan txmgr.SendResponse, len(t.bcasts))
ids := make([]common.Hash, len(t.bcasts))
for i, bcast := range t.bcasts {
futures[i], ids[i] = t.broadcast(ctx, bcast)
t.lgr.Info(
"transaction broadcasted",
"id", ids[i],
"nonce", bcast.Nonce,
)
}
var err *multierror.Error
var completed int
for i, fut := range futures {
bcastRes := <-fut
completed++
outRes := BroadcastResult{
Broadcast: t.bcasts[i],
}
if bcastRes.Err == nil {
outRes.Receipt = bcastRes.Receipt
outRes.TxHash = bcastRes.Receipt.TxHash
if bcastRes.Receipt.Status == 0 {
failErr := fmt.Errorf("transaction failed: %s", outRes.Receipt.TxHash.String())
err = multierror.Append(err, failErr)
outRes.Err = failErr
t.lgr.Error(
"transaction failed on chain",
"id", ids[i],
"completed", completed,
"total", len(t.bcasts),
"hash", outRes.Receipt.TxHash.String(),
"nonce", outRes.Broadcast.Nonce,
)
} else {
t.lgr.Info(
"transaction confirmed",
"id", ids[i],
"completed", completed,
"total", len(t.bcasts),
"hash", outRes.Receipt.TxHash.String(),
"nonce", outRes.Broadcast.Nonce,
"creation", outRes.Receipt.ContractAddress,
)
}
} else {
err = multierror.Append(err, bcastRes.Err)
outRes.Err = bcastRes.Err
t.lgr.Error(
"transaction failed",
"id", ids[i],
"completed", completed,
"total", len(t.bcasts),
"err", bcastRes.Err,
)
}
results = append(results, outRes)
}
return results, err.ErrorOrNil()
}
func (t *KeyedBroadcaster) broadcast(ctx context.Context, bcast script.Broadcast) (<-chan txmgr.SendResponse, common.Hash) {
id := bcast.ID()
value := ((*uint256.Int)(bcast.Value)).ToBig()
var candidate txmgr.TxCandidate
switch bcast.Type {
case script.BroadcastCall:
to := &bcast.To
candidate = txmgr.TxCandidate{
TxData: bcast.Input,
To: to,
Value: value,
GasLimit: padGasLimit(bcast.Input, bcast.GasUsed, false),
}
case script.BroadcastCreate:
candidate = txmgr.TxCandidate{
TxData: bcast.Input,
To: nil,
GasLimit: padGasLimit(bcast.Input, bcast.GasUsed, true),
}
}
ch := make(chan txmgr.SendResponse, 1)
t.mgr.SendAsync(ctx, candidate, ch)
return ch, id
}
func padGasLimit(data []byte, gasUsed uint64, creation bool) uint64 {
intrinsicGas, err := core.IntrinsicGas(data, nil, creation, true, true, false)
// This method never errors - we should look into it if it does.
if err != nil {
panic(err)
}
return uint64(float64(intrinsicGas+gasUsed) * GasPadFactor)
}
package deployer
import (
"os"
op_service "github.com/ethereum-optimism/optimism/op-service"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
"github.com/urfave/cli/v2"
)
const (
EnvVarPrefix = "DEPLOYER"
L1RPCURLFlagName = "l1-rpc-url"
L1ChainIDFlagName = "l1-chain-id"
WorkdirFlagName = "workdir"
OutdirFlagName = "outdir"
DevFlagName = "dev"
PrivateKeyFlagName = "private-key"
)
var (
L1RPCURLFlag = &cli.StringFlag{
Name: L1RPCURLFlagName,
Usage: "RPC URL for the L1 chain. Can be set to 'genesis' for deployments " +
"that will be deployed at the launch of the L1.",
EnvVars: []string{
"L1_RPC_URL",
},
}
L1ChainIDFlag = &cli.Uint64Flag{
Name: L1ChainIDFlagName,
Usage: "Chain ID of the L1 chain.",
EnvVars: prefixEnvVar("L1_CHAIN_ID"),
Value: 900,
}
WorkdirFlag = &cli.StringFlag{
Name: WorkdirFlagName,
Usage: "Directory storing intent and stage. Defaults to the current directory.",
EnvVars: prefixEnvVar("WORKDIR"),
Value: cwd(),
Aliases: []string{
OutdirFlagName,
},
}
DevFlag = &cli.BoolFlag{
Name: DevFlagName,
Usage: "Use development mode. This will use the development mnemonic to own the chain" +
" and fund dev accounts.",
EnvVars: prefixEnvVar("DEV"),
}
PrivateKeyFlag = &cli.StringFlag{
Name: PrivateKeyFlagName,
Usage: "Private key of the deployer account.",
EnvVars: prefixEnvVar("PRIVATE_KEY"),
}
)
var GlobalFlags = append([]cli.Flag{}, oplog.CLIFlags(EnvVarPrefix)...)
var InitFlags = []cli.Flag{
L1ChainIDFlag,
WorkdirFlag,
DevFlag,
}
var ApplyFlags = []cli.Flag{
L1RPCURLFlag,
WorkdirFlag,
PrivateKeyFlag,
}
func prefixEnvVar(name string) []string {
return op_service.PrefixEnvVar(EnvVarPrefix, name)
}
func cwd() string {
dir, err := os.Getwd()
if err != nil {
return ""
}
return dir
}
package deployer
import (
"fmt"
"path"
"github.com/ethereum-optimism/optimism/op-chain-ops/deployer/state"
"github.com/ethereum-optimism/optimism/op-chain-ops/devkeys"
"github.com/ethereum/go-ethereum/common"
"github.com/urfave/cli/v2"
)
const devMnemonic = "test test test test test test test test test test test junk"
type InitConfig struct {
L1ChainID uint64
Outdir string
Dev bool
}
func (c *InitConfig) Check() error {
if c.L1ChainID == 0 {
return fmt.Errorf("l1ChainID must be specified")
}
if c.Outdir == "" {
return fmt.Errorf("outdir must be specified")
}
return nil
}
func InitCLI() func(ctx *cli.Context) error {
return func(ctx *cli.Context) error {
l1ChainID := ctx.Uint64(L1ChainIDFlagName)
outdir := ctx.String(OutdirFlagName)
dev := ctx.Bool(DevFlagName)
return Init(InitConfig{
L1ChainID: l1ChainID,
Outdir: outdir,
Dev: dev,
})
}
}
func Init(cfg InitConfig) error {
if err := cfg.Check(); err != nil {
return fmt.Errorf("invalid config for init: %w", err)
}
intent := &state.Intent{
L1ChainID: cfg.L1ChainID,
UseFaultProofs: true,
FundDevAccounts: cfg.Dev,
}
l1ChainIDBig := intent.L1ChainIDBig()
if cfg.Dev {
dk, err := devkeys.NewMnemonicDevKeys(devMnemonic)
if err != nil {
return fmt.Errorf("failed to create dev keys: %w", err)
}
addrFor := func(key devkeys.Key) common.Address {
// The error below should never happen, so panic if it does.
addr, err := dk.Address(key)
if err != nil {
panic(err)
}
return addr
}
intent.SuperchainRoles = state.SuperchainRoles{
ProxyAdminOwner: addrFor(devkeys.L1ProxyAdminOwnerRole.Key(l1ChainIDBig)),
ProtocolVersionsOwner: addrFor(devkeys.SuperchainDeployerKey.Key(l1ChainIDBig)),
Guardian: addrFor(devkeys.SuperchainConfigGuardianKey.Key(l1ChainIDBig)),
}
}
st := &state.State{
Version: 1,
}
if err := intent.WriteToFile(path.Join(cfg.Outdir, "intent.toml")); err != nil {
return fmt.Errorf("failed to write intent to file: %w", err)
}
if err := st.WriteToFile(path.Join(cfg.Outdir, "state.json")); err != nil {
return fmt.Errorf("failed to write state to file: %w", err)
}
return nil
}
package opsm
import (
"context"
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-chain-ops/deployer/broadcaster"
"github.com/ethereum-optimism/optimism/op-chain-ops/foundry"
"github.com/ethereum-optimism/optimism/op-chain-ops/script"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
)
type DeploySuperchainInput struct {
ProxyAdminOwner common.Address `toml:"proxyAdminOwner"`
ProtocolVersionsOwner common.Address `toml:"protocolVersionsOwner"`
Guardian common.Address `toml:"guardian"`
Paused bool `toml:"paused"`
RequiredProtocolVersion params.ProtocolVersion `toml:"requiredProtocolVersion"`
RecommendedProtocolVersion params.ProtocolVersion `toml:"recommendedProtocolVersion"`
}
func (dsi *DeploySuperchainInput) InputSet() bool {
return true
}
type DeploySuperchainOutput struct {
SuperchainProxyAdmin common.Address `toml:"superchainProxyAdmin"`
SuperchainConfigImpl common.Address `toml:"superchainConfigImpl"`
SuperchainConfigProxy common.Address `toml:"superchainConfigProxy"`
ProtocolVersionsImpl common.Address `toml:"protocolVersionsImpl"`
ProtocolVersionsProxy common.Address `toml:"protocolVersionsProxy"`
}
func (output *DeploySuperchainOutput) CheckOutput() error {
return nil
}
type DeploySuperchainScript struct {
Run func(in common.Address, out common.Address) error
}
type DeploySuperchainOpts struct {
ChainID *big.Int
ArtifactsFS foundry.StatDirFs
Deployer common.Address
Signer opcrypto.SignerFn
Input DeploySuperchainInput
Client *ethclient.Client
Logger log.Logger
}
func DeploySuperchainForge(ctx context.Context, opts DeploySuperchainOpts) (DeploySuperchainOutput, error) {
var dso DeploySuperchainOutput
bcaster, err := broadcaster.NewKeyedBroadcaster(broadcaster.KeyedBroadcasterOpts{
Logger: opts.Logger,
ChainID: opts.ChainID,
Client: opts.Client,
Signer: opts.Signer,
From: opts.Deployer,
})
if err != nil {
return dso, fmt.Errorf("failed to create broadcaster: %w", err)
}
scriptCtx := script.DefaultContext
scriptCtx.Sender = opts.Deployer
scriptCtx.Origin = opts.Deployer
artifacts := &foundry.ArtifactsFS{FS: opts.ArtifactsFS}
h := script.NewHost(
opts.Logger,
artifacts,
nil,
scriptCtx,
script.WithBroadcastHook(bcaster.Hook),
script.WithIsolatedBroadcasts(),
)
if err := h.EnableCheats(); err != nil {
return dso, fmt.Errorf("failed to enable cheats: %w", err)
}
nonce, err := opts.Client.NonceAt(ctx, opts.Deployer, nil)
if err != nil {
return dso, fmt.Errorf("failed to get deployer nonce: %w", err)
}
inputAddr := h.NewScriptAddress()
outputAddr := h.NewScriptAddress()
cleanupInput, err := script.WithPrecompileAtAddress[*DeploySuperchainInput](h, inputAddr, &opts.Input)
if err != nil {
return dso, fmt.Errorf("failed to insert DeploySuperchainInput precompile: %w", err)
}
defer cleanupInput()
cleanupOutput, err := script.WithPrecompileAtAddress[*DeploySuperchainOutput](
h,
outputAddr,
&dso,
script.WithFieldSetter[*DeploySuperchainOutput],
)
if err != nil {
return dso, fmt.Errorf("failed to insert DeploySuperchainOutput precompile: %w", err)
}
defer cleanupOutput()
deployScript, cleanupDeploy, err := script.WithScript[DeploySuperchainScript](h, "DeploySuperchain.s.sol", "DeploySuperchain")
if err != nil {
return dso, fmt.Errorf("failed to load DeploySuperchain script: %w", err)
}
defer cleanupDeploy()
h.SetNonce(opts.Deployer, nonce)
opts.Logger.Info("deployer nonce", "nonce", nonce)
if err := deployScript.Run(inputAddr, outputAddr); err != nil {
return dso, fmt.Errorf("failed to run DeploySuperchain script: %w", err)
}
if _, err := bcaster.Broadcast(ctx); err != nil {
return dso, fmt.Errorf("failed to broadcast transactions: %w", err)
}
return dso, nil
}
package pipeline
import (
"context"
"fmt"
"path"
"github.com/ethereum-optimism/optimism/op-chain-ops/deployer/state"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
"github.com/ethereum-optimism/optimism/op-service/jsonutil"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
)
type Env struct {
Workdir string
L1Client *ethclient.Client
L1RPCUrl string
Signer opcrypto.SignerFn
Deployer common.Address
Logger log.Logger
}
func (e *Env) ReadIntent() (*state.Intent, error) {
intentPath := path.Join(e.Workdir, "intent.toml")
intent, err := jsonutil.LoadTOML[state.Intent](intentPath)
if err != nil {
return nil, fmt.Errorf("failed to read intent file: %w", err)
}
return intent, nil
}
func (e *Env) ReadState() (*state.State, error) {
statePath := path.Join(e.Workdir, "state.json")
st, err := jsonutil.LoadJSON[state.State](statePath)
if err != nil {
return nil, fmt.Errorf("failed to read state file: %w", err)
}
return st, nil
}
func (e *Env) WriteState(st *state.State) error {
statePath := path.Join(e.Workdir, "state.json")
return st.WriteToFile(statePath)
}
type Stage func(ctx context.Context, env *Env, intent *state.Intent, state2 *state.State) error
package pipeline
import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-chain-ops/deployer/state"
)
func IsSupportedStateVersion(version int) bool {
return version == 1
}
func Init(ctx context.Context, env *Env, intent *state.Intent, st *state.State) error {
lgr := env.Logger.New("stage", "init")
lgr.Info("initializing pipeline")
// Ensure the state version is supported.
if !IsSupportedStateVersion(st.Version) {
return fmt.Errorf("unsupported state version: %d", st.Version)
}
// If the state has never been applied, we don't need to perform
// any additional checks.
if st.AppliedIntent == nil {
return nil
}
// If the state has been applied, we need to check if any immutable
// fields have changed.
if st.AppliedIntent.L1ChainID != intent.L1ChainID {
return immutableErr("L1ChainID", st.AppliedIntent.L1ChainID, intent.L1ChainID)
}
if st.AppliedIntent.UseFaultProofs != intent.UseFaultProofs {
return immutableErr("useFaultProofs", st.AppliedIntent.UseFaultProofs, intent.UseFaultProofs)
}
if st.AppliedIntent.UseAltDA != intent.UseAltDA {
return immutableErr("useAltDA", st.AppliedIntent.UseAltDA, intent.UseAltDA)
}
if st.AppliedIntent.FundDevAccounts != intent.FundDevAccounts {
return immutableErr("fundDevAccounts", st.AppliedIntent.FundDevAccounts, intent.FundDevAccounts)
}
l1ChainID, err := env.L1Client.ChainID(ctx)
if err != nil {
return fmt.Errorf("failed to get L1 chain ID: %w", err)
}
if l1ChainID.Cmp(intent.L1ChainIDBig()) != 0 {
return fmt.Errorf("L1 chain ID mismatch: got %d, expected %d", l1ChainID, intent.L1ChainID)
}
// TODO: validate individual L2s
return nil
}
func immutableErr(field string, was, is any) error {
return fmt.Errorf("%s is immutable: was %v, is %v", field, was, is)
}
package pipeline
import (
"context"
"fmt"
"math/big"
"os"
"github.com/ethereum-optimism/optimism/op-chain-ops/deployer/opsm"
"github.com/ethereum-optimism/optimism/op-chain-ops/deployer/state"
"github.com/ethereum-optimism/optimism/op-chain-ops/foundry"
"github.com/ethereum-optimism/optimism/op-node/rollup"
)
const DefaultContractsBedrockRepo = "us-docker.pkg.dev/oplabs-tools-artifacts/images/contracts-bedrock"
func DeploySuperchain(ctx context.Context, env *Env, intent *state.Intent, st *state.State) error {
lgr := env.Logger.New("stage", "deploy-superchain")
if !shouldDeploySuperchain(intent, st) {
lgr.Info("superchain deployment not needed")
return nil
}
lgr.Info("deploying superchain")
var artifactsFS foundry.StatDirFs
var err error
if intent.ContractArtifactsURL.Scheme == "file" {
fs := os.DirFS(intent.ContractArtifactsURL.Path)
artifactsFS = fs.(foundry.StatDirFs)
} else {
return fmt.Errorf("only file:// artifacts URLs are supported")
}
dso, err := opsm.DeploySuperchainForge(
ctx,
opsm.DeploySuperchainOpts{
Input: opsm.DeploySuperchainInput{
ProxyAdminOwner: intent.SuperchainRoles.ProxyAdminOwner,
ProtocolVersionsOwner: intent.SuperchainRoles.ProtocolVersionsOwner,
Guardian: intent.SuperchainRoles.Guardian,
Paused: false,
RequiredProtocolVersion: rollup.OPStackSupport,
RecommendedProtocolVersion: rollup.OPStackSupport,
},
ArtifactsFS: artifactsFS,
ChainID: big.NewInt(int64(intent.L1ChainID)),
Client: env.L1Client,
Signer: env.Signer,
Deployer: env.Deployer,
Logger: lgr,
},
)
if err != nil {
return fmt.Errorf("error deploying superchain: %w", err)
}
st.SuperchainDeployment = &state.SuperchainDeployment{
ProxyAdminAddress: dso.SuperchainProxyAdmin,
SuperchainConfigProxyAddress: dso.SuperchainConfigProxy,
SuperchainConfigImplAddress: dso.SuperchainConfigImpl,
ProtocolVersionsProxyAddress: dso.ProtocolVersionsProxy,
ProtocolVersionsImplAddress: dso.ProtocolVersionsImpl,
}
if err := env.WriteState(st); err != nil {
return err
}
return nil
}
func shouldDeploySuperchain(intent *state.Intent, st *state.State) bool {
if st.AppliedIntent == nil {
return true
}
if st.SuperchainDeployment == nil {
return true
}
return false
}
package state
import "net/url"
type ArtifactsURL url.URL
func (a *ArtifactsURL) MarshalText() ([]byte, error) {
return []byte((*url.URL)(a).String()), nil
}
func (a *ArtifactsURL) UnmarshalText(text []byte) error {
u, err := url.Parse(string(text))
if err != nil {
return err
}
*a = ArtifactsURL(*u)
return nil
}
package state
import (
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-service/ioutil"
"github.com/ethereum-optimism/optimism/op-service/jsonutil"
"github.com/ethereum/go-ethereum/common"
)
var emptyAddress common.Address
type Intent struct {
L1ChainID uint64 `json:"l1ChainID" toml:"l1ChainID"`
SuperchainRoles SuperchainRoles `json:"superchainRoles" toml:"superchainRoles"`
UseFaultProofs bool `json:"useFaultProofs" toml:"useFaultProofs"`
UseAltDA bool `json:"useAltDA" toml:"useAltDA"`
FundDevAccounts bool `json:"fundDevAccounts" toml:"fundDevAccounts"`
ContractArtifactsURL *ArtifactsURL `json:"contractArtifactsURL" toml:"contractArtifactsURL"`
Chains []Chain `json:"chains" toml:"chains"`
}
func (c Intent) L1ChainIDBig() *big.Int {
return big.NewInt(int64(c.L1ChainID))
}
func (c Intent) Check() error {
if c.L1ChainID == 0 {
return fmt.Errorf("l1ChainID must be set")
}
if c.UseFaultProofs && c.UseAltDA {
return fmt.Errorf("cannot use both fault proofs and alt-DA")
}
if c.SuperchainRoles.ProxyAdminOwner == emptyAddress {
return fmt.Errorf("proxyAdminOwner must be set")
}
if c.SuperchainRoles.ProtocolVersionsOwner == emptyAddress {
c.SuperchainRoles.ProtocolVersionsOwner = c.SuperchainRoles.ProxyAdminOwner
}
if c.SuperchainRoles.Guardian == emptyAddress {
c.SuperchainRoles.Guardian = c.SuperchainRoles.ProxyAdminOwner
}
if c.ContractArtifactsURL == nil {
return fmt.Errorf("contractArtifactsURL must be set")
}
if c.ContractArtifactsURL.Scheme != "file" {
return fmt.Errorf("contractArtifactsURL must be a file URL")
}
return nil
}
func (c Intent) Chain(id uint64) (Chain, error) {
for i := range c.Chains {
if c.Chains[i].ID == id {
return c.Chains[i], nil
}
}
return Chain{}, fmt.Errorf("chain %d not found", id)
}
func (c Intent) WriteToFile(path string) error {
return jsonutil.WriteTOML(c, ioutil.ToAtomicFile(path, 0o755))
}
type SuperchainRoles struct {
ProxyAdminOwner common.Address `json:"proxyAdminOwner" toml:"proxyAdminOwner"`
ProtocolVersionsOwner common.Address `json:"protocolVersionsOwner" toml:"protocolVersionsOwner"`
Guardian common.Address `json:"guardian" toml:"guardian"`
}
type Chain struct {
ID uint64 `json:"id"`
Roles ChainRoles `json:"roles"`
Overrides map[string]any `json:"overrides"`
}
type ChainRoles struct {
ProxyAdminOwner common.Address `json:"proxyAdminOwner"`
SystemConfigOwner common.Address `json:"systemConfigOwner"`
GovernanceTokenOwner common.Address `json:"governanceTokenOwner"`
UnsafeBlockSigner common.Address `json:"unsafeBlockSigner"`
Batcher common.Address `json:"batcher"`
Proposer common.Address `json:"proposer"`
Challenger common.Address `json:"challenger"`
}
func (c *Chain) Check() error {
if c.ID == 0 {
return fmt.Errorf("id must be set")
}
if c.Roles.ProxyAdminOwner == emptyAddress {
return fmt.Errorf("proxyAdminOwner must be set")
}
if c.Roles.SystemConfigOwner == emptyAddress {
c.Roles.SystemConfigOwner = c.Roles.ProxyAdminOwner
}
if c.Roles.GovernanceTokenOwner == emptyAddress {
c.Roles.GovernanceTokenOwner = c.Roles.ProxyAdminOwner
}
if c.Roles.UnsafeBlockSigner == emptyAddress {
return fmt.Errorf("unsafeBlockSigner must be set")
}
if c.Roles.Batcher == emptyAddress {
return fmt.Errorf("batcher must be set")
}
return nil
}
package state
import (
"github.com/ethereum-optimism/optimism/op-service/ioutil"
"github.com/ethereum-optimism/optimism/op-service/jsonutil"
"github.com/ethereum/go-ethereum/common"
)
// State contains the data needed to recreate the deployment
// as it progresses and once it is fully applied.
type State struct {
// Version versions the state so we can update it later.
Version int `json:"version"`
// AppliedIntent contains the chain intent that was last
// successfully applied. It is diffed against new intent
// in order to determine what deployment steps to take.
// This field is nil for new deployments.
AppliedIntent *Intent `json:"appliedIntent"`
// SuperchainDeployment contains the addresses of the Superchain
// deployment. It only contains the proxies because the implementations
// can be looked up on chain.
SuperchainDeployment *SuperchainDeployment `json:"superchainDeployment"`
}
func (s State) WriteToFile(path string) error {
return jsonutil.WriteJSON(s, ioutil.ToAtomicFile(path, 0o755))
}
type SuperchainDeployment struct {
ProxyAdminAddress common.Address `json:"proxyAdminAddress"`
SuperchainConfigProxyAddress common.Address `json:"superchainConfigProxyAddress"`
SuperchainConfigImplAddress common.Address `json:"superchainConfigImplAddress"`
ProtocolVersionsProxyAddress common.Address `json:"protocolVersionsProxyAddress"`
ProtocolVersionsImplAddress common.Address `json:"protocolVersionsImplAddress"`
}
...@@ -49,6 +49,10 @@ func ChainUserKeys(chainID *big.Int) func(index uint64) ChainUserKey { ...@@ -49,6 +49,10 @@ func ChainUserKeys(chainID *big.Int) func(index uint64) ChainUserKey {
} }
} }
type Role interface {
Key(chainID *big.Int) Key
}
// SuperchainOperatorRole identifies an account used in the operations of superchain contracts // SuperchainOperatorRole identifies an account used in the operations of superchain contracts
type SuperchainOperatorRole uint64 type SuperchainOperatorRole uint64
...@@ -82,6 +86,13 @@ func (role SuperchainOperatorRole) String() string { ...@@ -82,6 +86,13 @@ func (role SuperchainOperatorRole) String() string {
} }
} }
func (role SuperchainOperatorRole) Key(chainID *big.Int) Key {
return &SuperchainOperatorKey{
ChainID: chainID,
Role: role,
}
}
// SuperchainOperatorKey is an account specific to an OperationRole of a given OP-Stack chain. // SuperchainOperatorKey is an account specific to an OperationRole of a given OP-Stack chain.
type SuperchainOperatorKey struct { type SuperchainOperatorKey struct {
ChainID *big.Int ChainID *big.Int
...@@ -163,7 +174,7 @@ func (role ChainOperatorRole) String() string { ...@@ -163,7 +174,7 @@ func (role ChainOperatorRole) String() string {
} }
} }
func (role ChainOperatorRole) Key(chainID *big.Int) *ChainOperatorKey { func (role ChainOperatorRole) Key(chainID *big.Int) Key {
return &ChainOperatorKey{ return &ChainOperatorKey{
ChainID: chainID, ChainID: chainID,
Role: role, Role: role,
......
...@@ -9,14 +9,14 @@ import ( ...@@ -9,14 +9,14 @@ import (
"strings" "strings"
) )
type statDirFs interface { type StatDirFs interface {
fs.StatFS fs.StatFS
fs.ReadDirFS fs.ReadDirFS
} }
func OpenArtifactsDir(dirPath string) *ArtifactsFS { func OpenArtifactsDir(dirPath string) *ArtifactsFS {
dir := os.DirFS(dirPath) dir := os.DirFS(dirPath)
if d, ok := dir.(statDirFs); !ok { if d, ok := dir.(StatDirFs); !ok {
panic("Go DirFS guarantees changed") panic("Go DirFS guarantees changed")
} else { } else {
return &ArtifactsFS{FS: d} return &ArtifactsFS{FS: d}
...@@ -29,7 +29,7 @@ func OpenArtifactsDir(dirPath string) *ArtifactsFS { ...@@ -29,7 +29,7 @@ func OpenArtifactsDir(dirPath string) *ArtifactsFS {
// See OpenArtifactsDir for reading from a local directory. // See OpenArtifactsDir for reading from a local directory.
// Alternative FS systems, like a tarball, may be used too. // Alternative FS systems, like a tarball, may be used too.
type ArtifactsFS struct { type ArtifactsFS struct {
FS statDirFs FS StatDirFs
} }
// ListArtifacts lists the artifacts. Each artifact matches a source-file name. // ListArtifacts lists the artifacts. Each artifact matches a source-file name.
......
...@@ -2,6 +2,8 @@ package script ...@@ -2,6 +2,8 @@ package script
import ( import (
"bytes" "bytes"
"crypto/sha256"
"encoding/binary"
"errors" "errors"
"fmt" "fmt"
"math/big" "math/big"
...@@ -205,6 +207,24 @@ type Broadcast struct { ...@@ -205,6 +207,24 @@ type Broadcast struct {
Nonce uint64 `json:"nonce"` // pre-state nonce of From, before any increment (always 0 if create2) Nonce uint64 `json:"nonce"` // pre-state nonce of From, before any increment (always 0 if create2)
} }
// ID returns a hash that can be used to identify the broadcast.
// This is used instead of the transaction hash since broadcasting
// tools can change gas limits and other fields which would change
// the resulting transaction hash.
func (b Broadcast) ID() common.Hash {
h := sha256.New()
_, _ = h.Write(b.From[:])
_, _ = h.Write(b.To[:])
_, _ = h.Write(b.Input)
_, _ = h.Write(((*uint256.Int)(b.Value)).Bytes())
_, _ = h.Write(b.Salt[:])
nonce := make([]byte, 8)
binary.BigEndian.PutUint64(nonce, b.Nonce)
_, _ = h.Write(nonce)
sum := h.Sum(nil)
return common.BytesToHash(sum)
}
// NewBroadcast creates a Broadcast from a parent callframe, and the completed child callframe. // NewBroadcast creates a Broadcast from a parent callframe, and the completed child callframe.
// This method is preferred to manually creating the struct since it correctly handles // This method is preferred to manually creating the struct since it correctly handles
// data that must be copied prior to being returned to prevent accidental mutation. // data that must be copied prior to being returned to prevent accidental mutation.
......
...@@ -35,6 +35,12 @@ func PrivateKeySignerFn(key *ecdsa.PrivateKey, chainID *big.Int) bind.SignerFn { ...@@ -35,6 +35,12 @@ func PrivateKeySignerFn(key *ecdsa.PrivateKey, chainID *big.Int) bind.SignerFn {
} }
} }
func SignerFnFromBind(fn bind.SignerFn) SignerFn {
return func(_ context.Context, address common.Address, tx *types.Transaction) (*types.Transaction, error) {
return fn(address, tx)
}
}
// SignerFn is a generic transaction signing function. It may be a remote signer so it takes a context. // SignerFn is a generic transaction signing function. It may be a remote signer so it takes a context.
// It also takes the address that should be used to sign the transaction with. // It also takes the address that should be used to sign the transaction with.
type SignerFn func(context.Context, common.Address, *types.Transaction) (*types.Transaction, error) type SignerFn func(context.Context, common.Address, *types.Transaction) (*types.Transaction, error)
......
...@@ -6,10 +6,105 @@ import ( ...@@ -6,10 +6,105 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/BurntSushi/toml"
"github.com/ethereum-optimism/optimism/op-service/ioutil" "github.com/ethereum-optimism/optimism/op-service/ioutil"
) )
type Decoder interface {
Decode(v interface{}) error
}
type DecoderFactory func(r io.Reader) Decoder
type Encoder interface {
Encode(v interface{}) error
}
type EncoderFactory func(w io.Writer) Encoder
type jsonDecoder struct {
d *json.Decoder
}
func newJSONDecoder(r io.Reader) Decoder {
return &jsonDecoder{
d: json.NewDecoder(r),
}
}
func (d *jsonDecoder) Decode(v interface{}) error {
if err := d.d.Decode(v); err != nil {
return fmt.Errorf("failed to decode JSON: %w", err)
}
if _, err := d.d.Token(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}
type tomlDecoder struct {
r io.Reader
}
func newTOMLDecoder(r io.Reader) Decoder {
return &tomlDecoder{
r: r,
}
}
func (d *tomlDecoder) Decode(v interface{}) error {
if _, err := toml.NewDecoder(d.r).Decode(v); err != nil {
return fmt.Errorf("failed to decode TOML: %w", err)
}
return nil
}
type jsonEncoder struct {
e *json.Encoder
}
func newJSONEncoder(w io.Writer) Encoder {
e := json.NewEncoder(w)
e.SetIndent("", " ")
return &jsonEncoder{
e: e,
}
}
func (e *jsonEncoder) Encode(v interface{}) error {
if err := e.e.Encode(v); err != nil {
return fmt.Errorf("failed to encode JSON: %w", err)
}
return nil
}
type tomlEncoder struct {
w io.Writer
}
func newTOMLEncoder(w io.Writer) Encoder {
return &tomlEncoder{
w: w,
}
}
func (e *tomlEncoder) Encode(v interface{}) error {
if err := toml.NewEncoder(e.w).Encode(v); err != nil {
return fmt.Errorf("failed to encode TOML: %w", err)
}
return nil
}
func LoadJSON[X any](inputPath string) (*X, error) { func LoadJSON[X any](inputPath string) (*X, error) {
return load[X](inputPath, newJSONDecoder)
}
func LoadTOML[X any](inputPath string) (*X, error) {
return load[X](inputPath, newTOMLDecoder)
}
func load[X any](inputPath string, dec DecoderFactory) (*X, error) {
if inputPath == "" { if inputPath == "" {
return nil, errors.New("no path specified") return nil, errors.New("no path specified")
} }
...@@ -20,18 +115,21 @@ func LoadJSON[X any](inputPath string) (*X, error) { ...@@ -20,18 +115,21 @@ func LoadJSON[X any](inputPath string) (*X, error) {
} }
defer f.Close() defer f.Close()
var state X var state X
decoder := json.NewDecoder(f) if err := dec(f).Decode(&state); err != nil {
if err := decoder.Decode(&state); err != nil {
return nil, fmt.Errorf("failed to decode file %q: %w", inputPath, err) return nil, fmt.Errorf("failed to decode file %q: %w", inputPath, err)
} }
// We are only expecting 1 JSON object - confirm there is no trailing data
if _, err := decoder.Token(); err != io.EOF {
return nil, fmt.Errorf("unexpected trailing data in file %q", inputPath)
}
return &state, nil return &state, nil
} }
func WriteJSON[X any](value X, target ioutil.OutputTarget) error { func WriteJSON[X any](value X, target ioutil.OutputTarget) error {
return write(value, target, newJSONEncoder)
}
func WriteTOML[X any](value X, target ioutil.OutputTarget) error {
return write(value, target, newTOMLEncoder)
}
func write[X any](value X, target ioutil.OutputTarget, enc EncoderFactory) error {
out, closer, abort, err := target() out, closer, abort, err := target()
if err != nil { if err != nil {
return err return err
...@@ -40,10 +138,8 @@ func WriteJSON[X any](value X, target ioutil.OutputTarget) error { ...@@ -40,10 +138,8 @@ func WriteJSON[X any](value X, target ioutil.OutputTarget) error {
return nil // No output stream selected so skip generating the content entirely return nil // No output stream selected so skip generating the content entirely
} }
defer abort() defer abort()
enc := json.NewEncoder(out) if err := enc(out).Encode(value); err != nil {
enc.SetIndent("", " ") return fmt.Errorf("failed to encode: %w", err)
if err := enc.Encode(value); err != nil {
return fmt.Errorf("failed to encode to JSON: %w", err)
} }
_, err = out.Write([]byte{'\n'}) _, err = out.Write([]byte{'\n'})
if err != nil { if err != nil {
......
...@@ -2,7 +2,6 @@ package jsonutil ...@@ -2,7 +2,6 @@ package jsonutil
import ( import (
"encoding/json" "encoding/json"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
...@@ -95,7 +94,7 @@ func TestLoadJSONWithExtraDataAppended(t *testing.T) { ...@@ -95,7 +94,7 @@ func TestLoadJSONWithExtraDataAppended(t *testing.T) {
var result *jsonTestData var result *jsonTestData
result, err = LoadJSON[jsonTestData](file) result, err = LoadJSON[jsonTestData](file)
require.ErrorContains(t, err, fmt.Sprintf("unexpected trailing data in file %q", file)) require.ErrorContains(t, err, "unexpected trailing data")
require.Nil(t, result) require.Nil(t, result)
}) })
} }
......
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