From cca29ce871b285d0dfa1f110a721fc32d98331aa Mon Sep 17 00:00:00 2001 From: Matthew Slipper Date: Mon, 16 Sep 2024 11:23:28 -0600 Subject: [PATCH] Add L2 genesis generation support to `op-deployer` Adds support for generating L2 genesis files to `op-deployer. The L2 initialization config is generated by merging in overrides as specified in the intent into a default config with sane values. The outputted genesis file is stored in the stage as a GZIP-compressed, base64-encoded string. --- op-chain-ops/deployer/apply.go | 5 + .../deployer/integration_test/apply_test.go | 6 +- .../deployers => deployer/opsm}/l2genesis.go | 2 +- op-chain-ops/deployer/pipeline/host.go | 25 +++- .../deployer/pipeline/implementations.go | 1 + op-chain-ops/deployer/pipeline/init.go | 10 ++ op-chain-ops/deployer/pipeline/l2genesis.go | 102 ++++++++++++++ op-chain-ops/deployer/pipeline/opchain.go | 3 +- op-chain-ops/deployer/pipeline/superchain.go | 1 + op-chain-ops/deployer/state/base64.go | 30 +++++ op-chain-ops/deployer/state/base64_test.go | 38 ++++++ op-chain-ops/deployer/state/intent.go | 17 ++- .../deployer/state/l2_initialization.go | 127 ++++++++++++++++++ op-chain-ops/deployer/state/state.go | 17 ++- op-chain-ops/interopgen/deploy.go | 4 +- 15 files changed, 367 insertions(+), 21 deletions(-) rename op-chain-ops/{interopgen/deployers => deployer/opsm}/l2genesis.go (99%) create mode 100644 op-chain-ops/deployer/pipeline/l2genesis.go create mode 100644 op-chain-ops/deployer/state/base64.go create mode 100644 op-chain-ops/deployer/state/base64_test.go create mode 100644 op-chain-ops/deployer/state/l2_initialization.go diff --git a/op-chain-ops/deployer/apply.go b/op-chain-ops/deployer/apply.go index e604eb8a9ffe..c3b374aa129e 100644 --- a/op-chain-ops/deployer/apply.go +++ b/op-chain-ops/deployer/apply.go @@ -149,6 +149,11 @@ func ApplyPipeline( func(ctx context.Context, env *pipeline.Env, intent *state.Intent, st *state.State) error { return pipeline.DeployOPChain(ctx, env, intent, st, chain.ID) }, + }, pipelineStage{ + fmt.Sprintf("generate-l2-genesis-%s", chain.ID.Hex()), + func(ctx context.Context, env *pipeline.Env, intent *state.Intent, st *state.State) error { + return pipeline.GenerateL2Genesis(ctx, env, intent, st, chain.ID) + }, }) } diff --git a/op-chain-ops/deployer/integration_test/apply_test.go b/op-chain-ops/deployer/integration_test/apply_test.go index ca03b4998004..84ad7d52c554 100644 --- a/op-chain-ops/deployer/integration_test/apply_test.go +++ b/op-chain-ops/deployer/integration_test/apply_test.go @@ -112,7 +112,7 @@ func TestEndToEndApply(t *testing.T) { UseFaultProofs: true, FundDevAccounts: true, ContractArtifactsURL: (*state.ArtifactsURL)(artifactsURL), - Chains: []state.ChainIntent{ + Chains: []*state.ChainIntent{ { ID: id.Bytes32(), Roles: state.ChainRoles{ @@ -196,5 +196,9 @@ func TestEndToEndApply(t *testing.T) { require.NotEmpty(t, code, "contracts %s at %s for chain %s has no code", addr.name, addr.addr, chainState.ID) }) } + + t.Run("l2 genesis", func(t *testing.T) { + require.Greater(t, len(chainState.Genesis), 0) + }) } } diff --git a/op-chain-ops/interopgen/deployers/l2genesis.go b/op-chain-ops/deployer/opsm/l2genesis.go similarity index 99% rename from op-chain-ops/interopgen/deployers/l2genesis.go rename to op-chain-ops/deployer/opsm/l2genesis.go index a80f4f69d74b..3567df71858f 100644 --- a/op-chain-ops/interopgen/deployers/l2genesis.go +++ b/op-chain-ops/deployer/opsm/l2genesis.go @@ -1,4 +1,4 @@ -package deployers +package opsm import ( "fmt" diff --git a/op-chain-ops/deployer/pipeline/host.go b/op-chain-ops/deployer/pipeline/host.go index f62a2ee8bfae..abea4e9499df 100644 --- a/op-chain-ops/deployer/pipeline/host.go +++ b/op-chain-ops/deployer/pipeline/host.go @@ -14,6 +14,22 @@ import ( "github.com/ethereum/go-ethereum/log" ) +type BroadcasterFactory func(opts CallScriptBroadcastOpts) (broadcaster.Broadcaster, error) + +func KeyedBroadcaster(opts CallScriptBroadcastOpts) (broadcaster.Broadcaster, error) { + return broadcaster.NewKeyedBroadcaster(broadcaster.KeyedBroadcasterOpts{ + Logger: opts.Logger, + ChainID: opts.L1ChainID, + Client: opts.Client, + Signer: opts.Signer, + From: opts.Deployer, + }) +} + +func DiscardBroadcaster(opts CallScriptBroadcastOpts) (broadcaster.Broadcaster, error) { + return broadcaster.DiscardBroadcaster(), nil +} + type CallScriptBroadcastOpts struct { L1ChainID *big.Int Logger log.Logger @@ -22,19 +38,14 @@ type CallScriptBroadcastOpts struct { Signer opcrypto.SignerFn Client *ethclient.Client Handler func(host *script.Host) error + Broadcaster BroadcasterFactory } func CallScriptBroadcast( ctx context.Context, opts CallScriptBroadcastOpts, ) error { - bcaster, err := broadcaster.NewKeyedBroadcaster(broadcaster.KeyedBroadcasterOpts{ - Logger: opts.Logger, - ChainID: opts.L1ChainID, - Client: opts.Client, - Signer: opts.Signer, - From: opts.Deployer, - }) + bcaster, err := opts.Broadcaster(opts) if err != nil { return fmt.Errorf("failed to create broadcaster: %w", err) } diff --git a/op-chain-ops/deployer/pipeline/implementations.go b/op-chain-ops/deployer/pipeline/implementations.go index e315dc6fbf1d..0a203c0f3119 100644 --- a/op-chain-ops/deployer/pipeline/implementations.go +++ b/op-chain-ops/deployer/pipeline/implementations.go @@ -42,6 +42,7 @@ func DeployImplementations(ctx context.Context, env *Env, intent *state.Intent, Deployer: env.Deployer, Signer: env.Signer, Client: env.L1Client, + Broadcaster: KeyedBroadcaster, Handler: func(host *script.Host) error { host.SetEnvVar("IMPL_SALT", st.Create2Salt.Hex()[2:]) host.ImportState(st.SuperchainDeployment.StateDump) diff --git a/op-chain-ops/deployer/pipeline/init.go b/op-chain-ops/deployer/pipeline/init.go index 55cbe9793d6a..26fbb7c2667c 100644 --- a/op-chain-ops/deployer/pipeline/init.go +++ b/op-chain-ops/deployer/pipeline/init.go @@ -5,6 +5,8 @@ import ( "crypto/rand" "fmt" + "github.com/ethereum-optimism/optimism/op-chain-ops/script" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum-optimism/optimism/op-chain-ops/deployer/state" @@ -63,6 +65,14 @@ func Init(ctx context.Context, env *Env, intent *state.Intent, st *state.State) return fmt.Errorf("L1 chain ID mismatch: got %d, expected %d", l1ChainID, intent.L1ChainID) } + deployerCode, err := env.L1Client.CodeAt(ctx, script.DeterministicDeployerAddress, nil) + if err != nil { + return fmt.Errorf("failed to get deployer code: %w", err) + } + if len(deployerCode) == 0 { + return fmt.Errorf("deterministic deployer is not deployed on this chain - please deploy it first") + } + // TODO: validate individual L2s return nil diff --git a/op-chain-ops/deployer/pipeline/l2genesis.go b/op-chain-ops/deployer/pipeline/l2genesis.go new file mode 100644 index 000000000000..b7f35d216324 --- /dev/null +++ b/op-chain-ops/deployer/pipeline/l2genesis.go @@ -0,0 +1,102 @@ +package pipeline + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/json" + "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-chain-ops/script" + "github.com/ethereum/go-ethereum/common" +) + +func GenerateL2Genesis(ctx context.Context, env *Env, intent *state.Intent, st *state.State, chainID common.Hash) error { + lgr := env.Logger.New("stage", "generate-l2-genesis") + + lgr.Info("generating L2 genesis", "id", chainID.Hex()) + + 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") + } + + thisIntent, err := intent.Chain(chainID) + if err != nil { + return fmt.Errorf("failed to get chain intent: %w", err) + } + + thisChainState, err := st.Chain(chainID) + if err != nil { + return fmt.Errorf("failed to get chain state: %w", err) + } + + initCfg, err := state.CombineL2InitConfig(intent, thisIntent) + if err != nil { + return fmt.Errorf("failed to combine L2 init config: %w", err) + } + + var dump *foundry.ForgeAllocs + err = CallScriptBroadcast( + ctx, + CallScriptBroadcastOpts{ + L1ChainID: big.NewInt(int64(intent.L1ChainID)), + Logger: lgr, + ArtifactsFS: artifactsFS, + Deployer: env.Deployer, + Signer: env.Signer, + Client: env.L1Client, + Broadcaster: DiscardBroadcaster, + Handler: func(host *script.Host) error { + err := opsm.L2Genesis(host, &opsm.L2GenesisInput{ + L1Deployments: opsm.L1Deployments{ + L1CrossDomainMessengerProxy: thisChainState.L1CrossDomainMessengerProxyAddress, + L1StandardBridgeProxy: thisChainState.L1StandardBridgeProxyAddress, + L1ERC721BridgeProxy: thisChainState.L1ERC721BridgeProxyAddress, + }, + L2Config: initCfg, + }) + if err != nil { + return fmt.Errorf("failed to call L2Genesis script: %w", err) + } + + host.Wipe(env.Deployer) + + dump, err = host.StateDump() + if err != nil { + return fmt.Errorf("failed to dump state: %w", err) + } + + return nil + }, + }, + ) + if err != nil { + return fmt.Errorf("failed to call L2Genesis script: %w", err) + } + + var buf bytes.Buffer + gw := gzip.NewWriter(&buf) + if err := json.NewEncoder(gw).Encode(dump); err != nil { + return fmt.Errorf("failed to encode state dump: %w", err) + } + if err := gw.Close(); err != nil { + return fmt.Errorf("failed to close gzip writer: %w", err) + } + thisChainState.Genesis = buf.Bytes() + + if err := env.WriteState(st); err != nil { + return fmt.Errorf("failed to write state: %w", err) + } + + return nil +} diff --git a/op-chain-ops/deployer/pipeline/opchain.go b/op-chain-ops/deployer/pipeline/opchain.go index 6a27ded2d520..c3a18e7b7383 100644 --- a/op-chain-ops/deployer/pipeline/opchain.go +++ b/op-chain-ops/deployer/pipeline/opchain.go @@ -47,6 +47,7 @@ func DeployOPChain(ctx context.Context, env *Env, intent *state.Intent, st *stat Deployer: env.Deployer, Signer: env.Signer, Client: env.L1Client, + Broadcaster: KeyedBroadcaster, Handler: func(host *script.Host) error { host.ImportState(st.ImplementationsDeployment.StateDump) dco, err = opsm.DeployOPChain( @@ -72,7 +73,7 @@ func DeployOPChain(ctx context.Context, env *Env, intent *state.Intent, st *stat return fmt.Errorf("error deploying OP chain: %w", err) } - st.Chains = append(st.Chains, state.ChainState{ + st.Chains = append(st.Chains, &state.ChainState{ ID: chainID, ProxyAdminAddress: dco.OpChainProxyAdmin, diff --git a/op-chain-ops/deployer/pipeline/superchain.go b/op-chain-ops/deployer/pipeline/superchain.go index 3a91c867ff3a..a3c1dc2827a3 100644 --- a/op-chain-ops/deployer/pipeline/superchain.go +++ b/op-chain-ops/deployer/pipeline/superchain.go @@ -44,6 +44,7 @@ func DeploySuperchain(ctx context.Context, env *Env, intent *state.Intent, st *s Deployer: env.Deployer, Signer: env.Signer, Client: env.L1Client, + Broadcaster: KeyedBroadcaster, Handler: func(host *script.Host) error { dso, err = opsm.DeploySuperchain( host, diff --git a/op-chain-ops/deployer/state/base64.go b/op-chain-ops/deployer/state/base64.go new file mode 100644 index 000000000000..23e4379fe5fa --- /dev/null +++ b/op-chain-ops/deployer/state/base64.go @@ -0,0 +1,30 @@ +package state + +import ( + "encoding/base64" + "encoding/json" +) + +type Base64Bytes []byte + +func (b Base64Bytes) MarshalJSON() ([]byte, error) { + if len(b) == 0 { + return []byte(`null`), nil + } + + encoded := base64.StdEncoding.EncodeToString(b) + return []byte(`"` + encoded + `"`), nil +} + +func (b *Base64Bytes) UnmarshalJSON(data []byte) error { + var dataStr string + if err := json.Unmarshal(data, &dataStr); err != nil { + return err + } + decoded, err := base64.StdEncoding.DecodeString(dataStr) + if err != nil { + return err + } + *b = decoded + return nil +} diff --git a/op-chain-ops/deployer/state/base64_test.go b/op-chain-ops/deployer/state/base64_test.go new file mode 100644 index 000000000000..abfa6654cee5 --- /dev/null +++ b/op-chain-ops/deployer/state/base64_test.go @@ -0,0 +1,38 @@ +package state + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBase64BytesMarshaling(t *testing.T) { + tests := []struct { + name string + in Base64Bytes + out string + }{ + { + name: "empty", + in: Base64Bytes{}, + out: "null", + }, + { + name: "non-empty", + in: Base64Bytes{0x01, 0x02, 0x03}, + out: `"AQID"`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := tt.in.MarshalJSON() + require.NoError(t, err) + require.Equal(t, tt.out, string(data)) + + var b Base64Bytes + err = b.UnmarshalJSON(data) + require.NoError(t, err) + require.Equal(t, tt.in, b) + }) + } +} diff --git a/op-chain-ops/deployer/state/intent.go b/op-chain-ops/deployer/state/intent.go index 3c41169e1d45..718026739945 100644 --- a/op-chain-ops/deployer/state/intent.go +++ b/op-chain-ops/deployer/state/intent.go @@ -24,13 +24,16 @@ type Intent struct { ContractArtifactsURL *ArtifactsURL `json:"contractArtifactsURL" toml:"contractArtifactsURL"` - Chains []ChainIntent `json:"chains" toml:"chains"` + Chains []*ChainIntent `json:"chains" toml:"chains"` + + GlobalInitOverrides map[string]any `json:"globalInitOverrides" toml:"globalInitOverrides"` } -func (c Intent) L1ChainIDBig() *big.Int { +func (c *Intent) L1ChainIDBig() *big.Int { return big.NewInt(int64(c.L1ChainID)) } -func (c Intent) Check() error { + +func (c *Intent) Check() error { if c.L1ChainID == 0 { return fmt.Errorf("l1ChainID must be set") } @@ -62,17 +65,17 @@ func (c Intent) Check() error { return nil } -func (c Intent) Chain(id common.Hash) (ChainIntent, error) { +func (c *Intent) Chain(id common.Hash) (*ChainIntent, error) { for i := range c.Chains { if c.Chains[i].ID == id { return c.Chains[i], nil } } - return ChainIntent{}, fmt.Errorf("chain %d not found", id) + return nil, fmt.Errorf("chain %d not found", id) } -func (c Intent) WriteToFile(path string) error { +func (c *Intent) WriteToFile(path string) error { return jsonutil.WriteTOML(c, ioutil.ToAtomicFile(path, 0o755)) } @@ -89,7 +92,7 @@ type ChainIntent struct { Roles ChainRoles `json:"roles" toml:"roles"` - Overrides map[string]any `json:"overrides" toml:"overrides"` + InitOverrides map[string]any `json:"initOverrides" toml:"initOverrides"` } type ChainRoles struct { diff --git a/op-chain-ops/deployer/state/l2_initialization.go b/op-chain-ops/deployer/state/l2_initialization.go new file mode 100644 index 000000000000..9baf98fec0cc --- /dev/null +++ b/op-chain-ops/deployer/state/l2_initialization.go @@ -0,0 +1,127 @@ +package state + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +var ( + l2GenesisBlockBaseFeePerGas = hexutil.Big(*(big.NewInt(1000000000))) + + vaultMinWithdrawalAmount = mustHexBigFromHex("0x8ac7230489e80000") +) + +func DefaultL2InitConfig() genesis.L2InitializationConfig { + return genesis.L2InitializationConfig{ + L2GenesisBlockDeployConfig: genesis.L2GenesisBlockDeployConfig{ + L2GenesisBlockGasLimit: 30_000_000, + L2GenesisBlockBaseFeePerGas: &l2GenesisBlockBaseFeePerGas, + }, + L2VaultsDeployConfig: genesis.L2VaultsDeployConfig{ + BaseFeeVaultWithdrawalNetwork: "local", + L1FeeVaultWithdrawalNetwork: "local", + SequencerFeeVaultWithdrawalNetwork: "local", + SequencerFeeVaultMinimumWithdrawalAmount: vaultMinWithdrawalAmount, + BaseFeeVaultMinimumWithdrawalAmount: vaultMinWithdrawalAmount, + L1FeeVaultMinimumWithdrawalAmount: vaultMinWithdrawalAmount, + }, + GovernanceDeployConfig: genesis.GovernanceDeployConfig{ + EnableGovernance: true, + GovernanceTokenSymbol: "OP", + GovernanceTokenName: "Optimism", + }, + GasPriceOracleDeployConfig: genesis.GasPriceOracleDeployConfig{ + GasPriceOracleBaseFeeScalar: 0, + GasPriceOracleBlobBaseFeeScalar: 1000000, + }, + EIP1559DeployConfig: genesis.EIP1559DeployConfig{ + EIP1559Denominator: 50, + EIP1559DenominatorCanyon: 250, + EIP1559Elasticity: 6, + }, + UpgradeScheduleDeployConfig: genesis.UpgradeScheduleDeployConfig{ + L2GenesisRegolithTimeOffset: u64UtilPtr(0), + L2GenesisCanyonTimeOffset: u64UtilPtr(0), + L2GenesisDeltaTimeOffset: u64UtilPtr(0), + L2GenesisEcotoneTimeOffset: u64UtilPtr(0), + L2GenesisFjordTimeOffset: u64UtilPtr(0), + L2GenesisGraniteTimeOffset: u64UtilPtr(0), + UseInterop: false, + }, + L2CoreDeployConfig: genesis.L2CoreDeployConfig{ + L2BlockTime: 2, + FinalizationPeriodSeconds: 12, + MaxSequencerDrift: 600, + SequencerWindowSize: 3600, + ChannelTimeoutBedrock: 300, + SystemConfigStartBlock: 0, + }, + } +} + +func CombineL2InitConfig(intent *Intent, chainIntent *ChainIntent) (genesis.L2InitializationConfig, error) { + cfg := DefaultL2InitConfig() + + var err error + if len(intent.GlobalInitOverrides) > 0 { + cfg, err = mergeJSON(cfg, intent.GlobalInitOverrides) + if err != nil { + return genesis.L2InitializationConfig{}, fmt.Errorf("error merging global L2 overrides: %w", err) + + } + } + + if len(chainIntent.InitOverrides) > 0 { + cfg, err = mergeJSON(cfg, chainIntent.InitOverrides) + if err != nil { + return genesis.L2InitializationConfig{}, fmt.Errorf("error merging chain L2 overrides: %w", err) + } + } + + return cfg, nil +} + +func mergeJSON[T any](in T, overrides ...map[string]any) (T, error) { + var out T + inJSON, err := json.Marshal(in) + if err != nil { + return out, err + } + + var tmpMap map[string]interface{} + if err := json.Unmarshal(inJSON, &tmpMap); err != nil { + return out, err + } + + for _, override := range overrides { + for k, v := range override { + tmpMap[k] = v + } + } + + inJSON, err = json.Marshal(tmpMap) + if err != nil { + return out, err + } + + if err := json.Unmarshal(inJSON, &out); err != nil { + return out, err + } + + return out, nil +} + +func mustHexBigFromHex(hex string) *hexutil.Big { + num := hexutil.MustDecodeBig(hex) + hexBig := hexutil.Big(*num) + return &hexBig +} + +func u64UtilPtr(in uint64) *hexutil.Uint64 { + util := hexutil.Uint64(in) + return &util +} diff --git a/op-chain-ops/deployer/state/state.go b/op-chain-ops/deployer/state/state.go index 315f25b64b76..75fbd60f2dc9 100644 --- a/op-chain-ops/deployer/state/state.go +++ b/op-chain-ops/deployer/state/state.go @@ -1,6 +1,8 @@ package state import ( + "fmt" + "github.com/ethereum-optimism/optimism/op-chain-ops/foundry" "github.com/ethereum-optimism/optimism/op-service/ioutil" "github.com/ethereum-optimism/optimism/op-service/jsonutil" @@ -32,13 +34,22 @@ type State struct { ImplementationsDeployment *ImplementationsDeployment `json:"implementationsDeployment"` // Chains contains data about L2 chain deployments. - Chains []ChainState `json:"opChainDeployments"` + Chains []*ChainState `json:"opChainDeployments"` } -func (s State) WriteToFile(path string) error { +func (s *State) WriteToFile(path string) error { return jsonutil.WriteJSON(s, ioutil.ToAtomicFile(path, 0o755)) } +func (s *State) Chain(id common.Hash) (*ChainState, error) { + for _, chain := range s.Chains { + if chain.ID == id { + return chain, nil + } + } + return nil, fmt.Errorf("chain not found: %s", id.Hex()) +} + type SuperchainDeployment struct { ProxyAdminAddress common.Address `json:"proxyAdminAddress"` SuperchainConfigProxyAddress common.Address `json:"superchainConfigProxyAddress"` @@ -82,4 +93,6 @@ type ChainState struct { PermissionedDisputeGameAddress common.Address `json:"permissionedDisputeGameAddress"` DelayedWETHPermissionedGameProxyAddress common.Address `json:"delayedWETHPermissionedGameProxyAddress"` DelayedWETHPermissionlessGameProxyAddress common.Address `json:"delayedWETHPermissionlessGameProxyAddress"` + + Genesis Base64Bytes `json:"genesis"` } diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index 65cb474d9f2b..d172184e91da 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -219,8 +219,8 @@ func deployL2ToL1(l1Host *script.Host, superCfg *SuperchainConfig, superDeployme } func genesisL2(l2Host *script.Host, cfg *L2Config, deployment *L2Deployment) error { - if err := deployers.L2Genesis(l2Host, &deployers.L2GenesisInput{ - L1Deployments: deployers.L1Deployments{ + if err := opsm.L2Genesis(l2Host, &opsm.L2GenesisInput{ + L1Deployments: opsm.L1Deployments{ L1CrossDomainMessengerProxy: deployment.L1CrossDomainMessengerProxy, L1StandardBridgeProxy: deployment.L1StandardBridgeProxy, L1ERC721BridgeProxy: deployment.L1ERC721BridgeProxy,