Commit 110a31db authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

Add L2 genesis generation support to `op-deployer` (#11938)

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.
parent f78ed7ba
...@@ -149,6 +149,11 @@ func ApplyPipeline( ...@@ -149,6 +149,11 @@ func ApplyPipeline(
func(ctx context.Context, env *pipeline.Env, intent *state.Intent, st *state.State) error { func(ctx context.Context, env *pipeline.Env, intent *state.Intent, st *state.State) error {
return pipeline.DeployOPChain(ctx, env, intent, st, chain.ID) 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)
},
}) })
} }
......
...@@ -112,7 +112,7 @@ func TestEndToEndApply(t *testing.T) { ...@@ -112,7 +112,7 @@ func TestEndToEndApply(t *testing.T) {
UseFaultProofs: true, UseFaultProofs: true,
FundDevAccounts: true, FundDevAccounts: true,
ContractArtifactsURL: (*state.ArtifactsURL)(artifactsURL), ContractArtifactsURL: (*state.ArtifactsURL)(artifactsURL),
Chains: []state.ChainIntent{ Chains: []*state.ChainIntent{
{ {
ID: id.Bytes32(), ID: id.Bytes32(),
Roles: state.ChainRoles{ Roles: state.ChainRoles{
...@@ -196,5 +196,9 @@ func TestEndToEndApply(t *testing.T) { ...@@ -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) 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)
})
} }
} }
...@@ -14,6 +14,22 @@ import ( ...@@ -14,6 +14,22 @@ import (
"github.com/ethereum/go-ethereum/log" "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 { type CallScriptBroadcastOpts struct {
L1ChainID *big.Int L1ChainID *big.Int
Logger log.Logger Logger log.Logger
...@@ -22,19 +38,14 @@ type CallScriptBroadcastOpts struct { ...@@ -22,19 +38,14 @@ type CallScriptBroadcastOpts struct {
Signer opcrypto.SignerFn Signer opcrypto.SignerFn
Client *ethclient.Client Client *ethclient.Client
Handler func(host *script.Host) error Handler func(host *script.Host) error
Broadcaster BroadcasterFactory
} }
func CallScriptBroadcast( func CallScriptBroadcast(
ctx context.Context, ctx context.Context,
opts CallScriptBroadcastOpts, opts CallScriptBroadcastOpts,
) error { ) error {
bcaster, err := broadcaster.NewKeyedBroadcaster(broadcaster.KeyedBroadcasterOpts{ bcaster, err := opts.Broadcaster(opts)
Logger: opts.Logger,
ChainID: opts.L1ChainID,
Client: opts.Client,
Signer: opts.Signer,
From: opts.Deployer,
})
if err != nil { if err != nil {
return fmt.Errorf("failed to create broadcaster: %w", err) return fmt.Errorf("failed to create broadcaster: %w", err)
} }
......
...@@ -42,6 +42,7 @@ func DeployImplementations(ctx context.Context, env *Env, intent *state.Intent, ...@@ -42,6 +42,7 @@ func DeployImplementations(ctx context.Context, env *Env, intent *state.Intent,
Deployer: env.Deployer, Deployer: env.Deployer,
Signer: env.Signer, Signer: env.Signer,
Client: env.L1Client, Client: env.L1Client,
Broadcaster: KeyedBroadcaster,
Handler: func(host *script.Host) error { Handler: func(host *script.Host) error {
host.SetEnvVar("IMPL_SALT", st.Create2Salt.Hex()[2:]) host.SetEnvVar("IMPL_SALT", st.Create2Salt.Hex()[2:])
host.ImportState(st.SuperchainDeployment.StateDump) host.ImportState(st.SuperchainDeployment.StateDump)
......
...@@ -5,6 +5,8 @@ import ( ...@@ -5,6 +5,8 @@ import (
"crypto/rand" "crypto/rand"
"fmt" "fmt"
"github.com/ethereum-optimism/optimism/op-chain-ops/script"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/op-chain-ops/deployer/state" "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) ...@@ -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) 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 // TODO: validate individual L2s
return nil return nil
......
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
}
...@@ -47,6 +47,7 @@ func DeployOPChain(ctx context.Context, env *Env, intent *state.Intent, st *stat ...@@ -47,6 +47,7 @@ func DeployOPChain(ctx context.Context, env *Env, intent *state.Intent, st *stat
Deployer: env.Deployer, Deployer: env.Deployer,
Signer: env.Signer, Signer: env.Signer,
Client: env.L1Client, Client: env.L1Client,
Broadcaster: KeyedBroadcaster,
Handler: func(host *script.Host) error { Handler: func(host *script.Host) error {
host.ImportState(st.ImplementationsDeployment.StateDump) host.ImportState(st.ImplementationsDeployment.StateDump)
dco, err = opsm.DeployOPChain( dco, err = opsm.DeployOPChain(
...@@ -72,7 +73,7 @@ func DeployOPChain(ctx context.Context, env *Env, intent *state.Intent, st *stat ...@@ -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) 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, ID: chainID,
ProxyAdminAddress: dco.OpChainProxyAdmin, ProxyAdminAddress: dco.OpChainProxyAdmin,
......
...@@ -44,6 +44,7 @@ func DeploySuperchain(ctx context.Context, env *Env, intent *state.Intent, st *s ...@@ -44,6 +44,7 @@ func DeploySuperchain(ctx context.Context, env *Env, intent *state.Intent, st *s
Deployer: env.Deployer, Deployer: env.Deployer,
Signer: env.Signer, Signer: env.Signer,
Client: env.L1Client, Client: env.L1Client,
Broadcaster: KeyedBroadcaster,
Handler: func(host *script.Host) error { Handler: func(host *script.Host) error {
dso, err = opsm.DeploySuperchain( dso, err = opsm.DeploySuperchain(
host, host,
......
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
}
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)
})
}
}
...@@ -24,13 +24,16 @@ type Intent struct { ...@@ -24,13 +24,16 @@ type Intent struct {
ContractArtifactsURL *ArtifactsURL `json:"contractArtifactsURL" toml:"contractArtifactsURL"` 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)) return big.NewInt(int64(c.L1ChainID))
} }
func (c Intent) Check() error {
func (c *Intent) Check() error {
if c.L1ChainID == 0 { if c.L1ChainID == 0 {
return fmt.Errorf("l1ChainID must be set") return fmt.Errorf("l1ChainID must be set")
} }
...@@ -62,17 +65,17 @@ func (c Intent) Check() error { ...@@ -62,17 +65,17 @@ func (c Intent) Check() error {
return nil 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 { for i := range c.Chains {
if c.Chains[i].ID == id { if c.Chains[i].ID == id {
return c.Chains[i], nil 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)) return jsonutil.WriteTOML(c, ioutil.ToAtomicFile(path, 0o755))
} }
...@@ -89,7 +92,7 @@ type ChainIntent struct { ...@@ -89,7 +92,7 @@ type ChainIntent struct {
Roles ChainRoles `json:"roles" toml:"roles"` 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 { type ChainRoles struct {
......
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
}
package state package state
import ( import (
"fmt"
"github.com/ethereum-optimism/optimism/op-chain-ops/foundry" "github.com/ethereum-optimism/optimism/op-chain-ops/foundry"
"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"
...@@ -32,13 +34,22 @@ type State struct { ...@@ -32,13 +34,22 @@ type State struct {
ImplementationsDeployment *ImplementationsDeployment `json:"implementationsDeployment"` ImplementationsDeployment *ImplementationsDeployment `json:"implementationsDeployment"`
// Chains contains data about L2 chain deployments. // 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)) 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 { type SuperchainDeployment struct {
ProxyAdminAddress common.Address `json:"proxyAdminAddress"` ProxyAdminAddress common.Address `json:"proxyAdminAddress"`
SuperchainConfigProxyAddress common.Address `json:"superchainConfigProxyAddress"` SuperchainConfigProxyAddress common.Address `json:"superchainConfigProxyAddress"`
...@@ -82,4 +93,6 @@ type ChainState struct { ...@@ -82,4 +93,6 @@ type ChainState struct {
PermissionedDisputeGameAddress common.Address `json:"permissionedDisputeGameAddress"` PermissionedDisputeGameAddress common.Address `json:"permissionedDisputeGameAddress"`
DelayedWETHPermissionedGameProxyAddress common.Address `json:"delayedWETHPermissionedGameProxyAddress"` DelayedWETHPermissionedGameProxyAddress common.Address `json:"delayedWETHPermissionedGameProxyAddress"`
DelayedWETHPermissionlessGameProxyAddress common.Address `json:"delayedWETHPermissionlessGameProxyAddress"` DelayedWETHPermissionlessGameProxyAddress common.Address `json:"delayedWETHPermissionlessGameProxyAddress"`
Genesis Base64Bytes `json:"genesis"`
} }
...@@ -219,8 +219,8 @@ func deployL2ToL1(l1Host *script.Host, superCfg *SuperchainConfig, superDeployme ...@@ -219,8 +219,8 @@ func deployL2ToL1(l1Host *script.Host, superCfg *SuperchainConfig, superDeployme
} }
func genesisL2(l2Host *script.Host, cfg *L2Config, deployment *L2Deployment) error { func genesisL2(l2Host *script.Host, cfg *L2Config, deployment *L2Deployment) error {
if err := deployers.L2Genesis(l2Host, &deployers.L2GenesisInput{ if err := opsm.L2Genesis(l2Host, &opsm.L2GenesisInput{
L1Deployments: deployers.L1Deployments{ L1Deployments: opsm.L1Deployments{
L1CrossDomainMessengerProxy: deployment.L1CrossDomainMessengerProxy, L1CrossDomainMessengerProxy: deployment.L1CrossDomainMessengerProxy,
L1StandardBridgeProxy: deployment.L1StandardBridgeProxy, L1StandardBridgeProxy: deployment.L1StandardBridgeProxy,
L1ERC721BridgeProxy: deployment.L1ERC721BridgeProxy, L1ERC721BridgeProxy: deployment.L1ERC721BridgeProxy,
......
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