diff --git a/op-chain-ops/deployer/broadcaster/keyed.go b/op-chain-ops/deployer/broadcaster/keyed.go index 2784c4d455be7..63b72010042b6 100644 --- a/op-chain-ops/deployer/broadcaster/keyed.go +++ b/op-chain-ops/deployer/broadcaster/keyed.go @@ -162,7 +162,7 @@ func (t *KeyedBroadcaster) Broadcast(ctx context.Context) ([]BroadcastResult, er ) } - results = append(results, outRes) + results[i] = outRes } return results, txErr.ErrorOrNil() } diff --git a/op-chain-ops/deployer/init.go b/op-chain-ops/deployer/init.go index 0cc288b40ffba..bd79f980cdffb 100644 --- a/op-chain-ops/deployer/init.go +++ b/op-chain-ops/deployer/init.go @@ -65,9 +65,10 @@ func Init(cfg InitConfig) error { } intent := &state.Intent{ - L1ChainID: cfg.L1ChainID, - UseFaultProofs: true, - FundDevAccounts: true, + L1ChainID: cfg.L1ChainID, + UseFaultProofs: true, + FundDevAccounts: true, + ContractsRelease: "dev", } l1ChainIDBig := intent.L1ChainIDBig() diff --git a/op-chain-ops/deployer/integration_test/apply_test.go b/op-chain-ops/deployer/integration_test/apply_test.go index 4399e0b887d34..ad22651fa36ed 100644 --- a/op-chain-ops/deployer/integration_test/apply_test.go +++ b/op-chain-ops/deployer/integration_test/apply_test.go @@ -93,18 +93,104 @@ func TestEndToEndApply(t *testing.T) { id := uint256.NewInt(1) - addrFor := func(key devkeys.Key) common.Address { - addr, err := dk.Address(key) - require.NoError(t, err) - return addr - } + deployerAddr, err := dk.Address(depKey) + require.NoError(t, err) + env := &pipeline.Env{ Workdir: t.TempDir(), L1Client: l1Client, Signer: signer, - Deployer: addrFor(depKey), + Deployer: deployerAddr, Logger: lgr, } + + t.Run("initial chain", func(t *testing.T) { + intent, st := makeIntent(t, l1ChainID, artifactsURL, dk, id) + + require.NoError(t, deployer.ApplyPipeline( + ctx, + env, + intent, + st, + )) + + addrs := []struct { + name string + addr common.Address + }{ + {"SuperchainProxyAdmin", st.SuperchainDeployment.ProxyAdminAddress}, + {"SuperchainConfigProxy", st.SuperchainDeployment.SuperchainConfigProxyAddress}, + {"SuperchainConfigImpl", st.SuperchainDeployment.SuperchainConfigImplAddress}, + {"ProtocolVersionsProxy", st.SuperchainDeployment.ProtocolVersionsProxyAddress}, + {"ProtocolVersionsImpl", st.SuperchainDeployment.ProtocolVersionsImplAddress}, + {"OpcmProxy", st.ImplementationsDeployment.OpcmProxyAddress}, + {"DelayedWETHImpl", st.ImplementationsDeployment.DelayedWETHImplAddress}, + {"OptimismPortalImpl", st.ImplementationsDeployment.OptimismPortalImplAddress}, + {"PreimageOracleSingleton", st.ImplementationsDeployment.PreimageOracleSingletonAddress}, + {"MipsSingleton", st.ImplementationsDeployment.MipsSingletonAddress}, + {"SystemConfigImpl", st.ImplementationsDeployment.SystemConfigImplAddress}, + {"L1CrossDomainMessengerImpl", st.ImplementationsDeployment.L1CrossDomainMessengerImplAddress}, + {"L1ERC721BridgeImpl", st.ImplementationsDeployment.L1ERC721BridgeImplAddress}, + {"L1StandardBridgeImpl", st.ImplementationsDeployment.L1StandardBridgeImplAddress}, + {"OptimismMintableERC20FactoryImpl", st.ImplementationsDeployment.OptimismMintableERC20FactoryImplAddress}, + {"DisputeGameFactoryImpl", st.ImplementationsDeployment.DisputeGameFactoryImplAddress}, + } + for _, addr := range addrs { + t.Run(addr.name, func(t *testing.T) { + code, err := l1Client.CodeAt(ctx, addr.addr, nil) + require.NoError(t, err) + require.NotEmpty(t, code, "contracts %s at %s has no code", addr.name, addr.addr) + }) + } + + validateOPChainDeployment(t, ctx, l1Client, st) + }) + + t.Run("subsequent chain", func(t *testing.T) { + newID := uint256.NewInt(2) + intent, st := makeIntent(t, l1ChainID, artifactsURL, dk, newID) + env.Workdir = t.TempDir() + + require.NoError(t, deployer.ApplyPipeline( + ctx, + env, + intent, + st, + )) + + addrs := []struct { + name string + addr common.Address + }{ + {"SuperchainConfigProxy", st.SuperchainDeployment.SuperchainConfigProxyAddress}, + {"ProtocolVersionsProxy", st.SuperchainDeployment.ProtocolVersionsProxyAddress}, + {"OpcmProxy", st.ImplementationsDeployment.OpcmProxyAddress}, + } + for _, addr := range addrs { + t.Run(addr.name, func(t *testing.T) { + code, err := l1Client.CodeAt(ctx, addr.addr, nil) + require.NoError(t, err) + require.NotEmpty(t, code, "contracts %s at %s has no code", addr.name, addr.addr) + }) + } + + validateOPChainDeployment(t, ctx, l1Client, st) + }) +} + +func makeIntent( + t *testing.T, + l1ChainID *big.Int, + artifactsURL *url.URL, + dk *devkeys.MnemonicDevKeys, + l2ChainID *uint256.Int, +) (*state.Intent, *state.State) { + addrFor := func(key devkeys.Key) common.Address { + addr, err := dk.Address(key) + require.NoError(t, err) + return addr + } + intent := &state.Intent{ L1ChainID: l1ChainID.Uint64(), SuperchainRoles: state.SuperchainRoles{ @@ -118,7 +204,7 @@ func TestEndToEndApply(t *testing.T) { ContractsRelease: "dev", Chains: []*state.ChainIntent{ { - ID: id.Bytes32(), + ID: l2ChainID.Bytes32(), Roles: state.ChainRoles{ ProxyAdminOwner: addrFor(devkeys.L2ProxyAdminOwnerRole.Key(l1ChainID)), SystemConfigOwner: addrFor(devkeys.SystemConfigOwner.Key(l1ChainID)), @@ -134,43 +220,10 @@ func TestEndToEndApply(t *testing.T) { st := &state.State{ Version: 1, } + return intent, st +} - require.NoError(t, deployer.ApplyPipeline( - ctx, - env, - intent, - st, - )) - - addrs := []struct { - name string - addr common.Address - }{ - {"SuperchainProxyAdmin", st.SuperchainDeployment.ProxyAdminAddress}, - {"SuperchainConfigProxy", st.SuperchainDeployment.SuperchainConfigProxyAddress}, - {"SuperchainConfigImpl", st.SuperchainDeployment.SuperchainConfigImplAddress}, - {"ProtocolVersionsProxy", st.SuperchainDeployment.ProtocolVersionsProxyAddress}, - {"ProtocolVersionsImpl", st.SuperchainDeployment.ProtocolVersionsImplAddress}, - {"OpcmProxy", st.ImplementationsDeployment.OpcmProxyAddress}, - {"DelayedWETHImpl", st.ImplementationsDeployment.DelayedWETHImplAddress}, - {"OptimismPortalImpl", st.ImplementationsDeployment.OptimismPortalImplAddress}, - {"PreimageOracleSingleton", st.ImplementationsDeployment.PreimageOracleSingletonAddress}, - {"MipsSingleton", st.ImplementationsDeployment.MipsSingletonAddress}, - {"SystemConfigImpl", st.ImplementationsDeployment.SystemConfigImplAddress}, - {"L1CrossDomainMessengerImpl", st.ImplementationsDeployment.L1CrossDomainMessengerImplAddress}, - {"L1ERC721BridgeImpl", st.ImplementationsDeployment.L1ERC721BridgeImplAddress}, - {"L1StandardBridgeImpl", st.ImplementationsDeployment.L1StandardBridgeImplAddress}, - {"OptimismMintableERC20FactoryImpl", st.ImplementationsDeployment.OptimismMintableERC20FactoryImplAddress}, - {"DisputeGameFactoryImpl", st.ImplementationsDeployment.DisputeGameFactoryImplAddress}, - } - for _, addr := range addrs { - t.Run(addr.name, func(t *testing.T) { - code, err := l1Client.CodeAt(ctx, addr.addr, nil) - require.NoError(t, err) - require.NotEmpty(t, code, "contracts %s at %s has no code", addr.name, addr.addr) - }) - } - +func validateOPChainDeployment(t *testing.T, ctx context.Context, l1Client *ethclient.Client, st *state.State) { for _, chainState := range st.Chains { chainAddrs := []struct { name string @@ -197,7 +250,7 @@ func TestEndToEndApply(t *testing.T) { if addr.name == "FaultDisputeGameAddress" { continue } - t.Run(fmt.Sprintf("chain %s - %s", chainState.ID, addr.name), func(t *testing.T) { + t.Run(addr.name, func(t *testing.T) { code, err := l1Client.CodeAt(ctx, addr.addr, nil) require.NoError(t, err) require.NotEmpty(t, code, "contracts %s at %s for chain %s has no code", addr.name, addr.addr, chainState.ID) diff --git a/op-chain-ops/deployer/opcm/contract.go b/op-chain-ops/deployer/opcm/contract.go new file mode 100644 index 0000000000000..c81222aafe888 --- /dev/null +++ b/op-chain-ops/deployer/opcm/contract.go @@ -0,0 +1,83 @@ +package opcm + +import ( + "bytes" + "context" + "fmt" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +type Contract struct { + addr common.Address + client *ethclient.Client +} + +func NewContract(addr common.Address, client *ethclient.Client) *Contract { + return &Contract{addr: addr, client: client} +} + +func (c *Contract) SuperchainConfig(ctx context.Context) (common.Address, error) { + return c.getAddress(ctx, "superchainConfig") +} + +func (c *Contract) ProtocolVersions(ctx context.Context) (common.Address, error) { + return c.getAddress(ctx, "protocolVersions") +} + +func (c *Contract) getAddress(ctx context.Context, name string) (common.Address, error) { + method := abi.NewMethod( + name, + name, + abi.Function, + "view", + true, + false, + abi.Arguments{}, + abi.Arguments{ + abi.Argument{ + Name: "address", + Type: mustType("address"), + Indexed: false, + }, + }, + ) + + calldata, err := method.Inputs.Pack() + if err != nil { + return common.Address{}, fmt.Errorf("failed to pack inputs: %w", err) + } + + msg := ethereum.CallMsg{ + To: &c.addr, + Data: append(bytes.Clone(method.ID), calldata...), + } + result, err := c.client.CallContract(ctx, msg, nil) + if err != nil { + return common.Address{}, fmt.Errorf("failed to call contract: %w", err) + } + + out, err := method.Outputs.Unpack(result) + if err != nil { + return common.Address{}, fmt.Errorf("failed to unpack result: %w", err) + } + if len(out) != 1 { + return common.Address{}, fmt.Errorf("unexpected output length: %d", len(out)) + } + addr, ok := out[0].(common.Address) + if !ok { + return common.Address{}, fmt.Errorf("unexpected type: %T", out[0]) + } + return addr, nil +} + +func mustType(t string) abi.Type { + typ, err := abi.NewType(t, "", nil) + if err != nil { + panic(err) + } + return typ +} diff --git a/op-chain-ops/deployer/opcm/opchain.go b/op-chain-ops/deployer/opcm/opchain.go index d9685182b6e13..c204c1a57ec3d 100644 --- a/op-chain-ops/deployer/opcm/opchain.go +++ b/op-chain-ops/deployer/opcm/opchain.go @@ -1,12 +1,19 @@ package opcm import ( + "context" "fmt" "math/big" + "strings" - "github.com/ethereum/go-ethereum/common" - + "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" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/holiman/uint256" ) // PermissionedGameStartingAnchorRoots is a root of bytes32(hex"dead") for the permissioned game at block 0, @@ -45,7 +52,6 @@ type DeployOPChainOutput struct { OptimismMintableERC20FactoryProxy common.Address L1StandardBridgeProxy common.Address L1CrossDomainMessengerProxy common.Address - // Fault proof contracts below. OptimismPortalProxy common.Address DisputeGameFactoryProxy common.Address @@ -97,3 +103,203 @@ func DeployOPChain(host *script.Host, input DeployOPChainInput) (DeployOPChainOu return dco, nil } + +// opcmRoles is an internal struct used to pass the roles to OPSM. See opcmDeployInput for more info. +type opcmRoles struct { + OpChainProxyAdminOwner common.Address + SystemConfigOwner common.Address + Batcher common.Address + UnsafeBlockSigner common.Address + Proposer common.Address + Challenger common.Address +} + +// opcmDeployInput is the input struct for the deploy method of the OPStackManager contract. We +// define a separate struct here to match what the OPSM contract expects. +type opcmDeployInput struct { + Roles opcmRoles + BasefeeScalar uint32 + BlobBasefeeScalar uint32 + L2ChainId *big.Int + StartingAnchorRoots []byte +} + +// decodeOutputABIJSON defines an ABI for a fake method called "decodeOutput" that returns the +// DeployOutput struct. This allows the code in the deployer to decode directly into a struct +// using Geth's ABI library. +const decodeOutputABIJSON = ` +[ + { + "type": "function", + "name": "decodeOutput", + "inputs": [], + "outputs": [ + { + "name": "output", + "indexed": false, + "type": "tuple", + "components": [ + { + "name": "opChainProxyAdmin", + "type": "address" + }, + { + "name": "addressManager", + "type": "address" + }, + { + "name": "l1ERC721BridgeProxy", + "type": "address" + }, + { + "name": "systemConfigProxy", + "type": "address" + }, + { + "name": "optimismMintableERC20FactoryProxy", + "type": "address" + }, + { + "name": "l1StandardBridgeProxy", + "type": "address" + }, + { + "name": "l1CrossDomainMessengerProxy", + "type": "address" + }, + { + "name": "optimismPortalProxy", + "type": "address" + }, + { + "name": "disputeGameFactoryProxy", + "type": "address" + }, + { + "name": "anchorStateRegistryProxy", + "type": "address" + }, + { + "name": "anchorStateRegistryImpl", + "type": "address" + }, + { + "name": "faultDisputeGame", + "type": "address", + "internalType": "contract FaultDisputeGame" + }, + { + "name": "permissionedDisputeGame", + "type": "address" + }, + { + "name": "delayedWETHPermissionedGameProxy", + "type": "address" + }, + { + "name": "delayedWETHPermissionlessGameProxy", + "type": "address" + } + ] + } + ] + } +] +` + +var decodeOutputABI abi.ABI + +// DeployOPChainRaw deploys an OP Chain using a raw call to a pre-deployed OPSM contract. +func DeployOPChainRaw( + ctx context.Context, + l1 *ethclient.Client, + bcast broadcaster.Broadcaster, + deployer common.Address, + artifacts foundry.StatDirFs, + input DeployOPChainInput, +) (DeployOPChainOutput, error) { + var out DeployOPChainOutput + + artifactsFS := &foundry.ArtifactsFS{FS: artifacts} + opcmArtifacts, err := artifactsFS.ReadArtifact("OPContractsManager.sol", "OPContractsManager") + if err != nil { + return out, fmt.Errorf("failed to read OPStackManager artifact: %w", err) + } + + opcmABI := opcmArtifacts.ABI + calldata, err := opcmABI.Pack("deploy", opcmDeployInput{ + Roles: opcmRoles{ + OpChainProxyAdminOwner: input.OpChainProxyAdminOwner, + SystemConfigOwner: input.SystemConfigOwner, + Batcher: input.Batcher, + UnsafeBlockSigner: input.UnsafeBlockSigner, + Proposer: input.Proposer, + Challenger: input.Challenger, + }, + BasefeeScalar: input.BasefeeScalar, + BlobBasefeeScalar: input.BlobBaseFeeScalar, + L2ChainId: input.L2ChainId, + StartingAnchorRoots: input.StartingAnchorRoots(), + }) + if err != nil { + return out, fmt.Errorf("failed to pack deploy input: %w", err) + } + + nonce, err := l1.NonceAt(ctx, deployer, nil) + if err != nil { + return out, fmt.Errorf("failed to read nonce: %w", err) + } + + bcast.Hook(script.Broadcast{ + From: deployer, + To: input.OpcmProxy, + Input: calldata, + Value: (*hexutil.U256)(uint256.NewInt(0)), + // use hardcoded 19MM gas for now since this is roughly what we've seen this deployment cost. + GasUsed: 19_000_000, + Type: script.BroadcastCall, + Nonce: nonce, + }) + + results, err := bcast.Broadcast(ctx) + if err != nil { + return out, fmt.Errorf("failed to broadcast OP chain deployment: %w", err) + } + + deployedEvent := opcmABI.Events["Deployed"] + res := results[0] + + for _, log := range res.Receipt.Logs { + if log.Topics[0] != deployedEvent.ID { + continue + } + + type EventData struct { + DeployOutput []byte + } + var data EventData + if err := opcmABI.UnpackIntoInterface(&data, "Deployed", log.Data); err != nil { + return out, fmt.Errorf("failed to unpack Deployed event: %w", err) + } + + type OutputData struct { + Output DeployOPChainOutput + } + var outData OutputData + if err := decodeOutputABI.UnpackIntoInterface(&outData, "decodeOutput", data.DeployOutput); err != nil { + return out, fmt.Errorf("failed to unpack DeployOutput: %w", err) + } + + return outData.Output, nil + } + + return out, fmt.Errorf("failed to find Deployed event") +} + +func init() { + var err error + decodeOutputABI, err = abi.JSON(strings.NewReader(decodeOutputABIJSON)) + if err != nil { + panic(fmt.Sprintf("failed to parse decodeOutput ABI: %v", err)) + } +} diff --git a/op-chain-ops/deployer/pipeline/init.go b/op-chain-ops/deployer/pipeline/init.go index 094e103aa940c..a680c7fdb48f8 100644 --- a/op-chain-ops/deployer/pipeline/init.go +++ b/op-chain-ops/deployer/pipeline/init.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "fmt" + "github.com/ethereum-optimism/optimism/op-chain-ops/deployer/opcm" "github.com/ethereum-optimism/optimism/op-chain-ops/foundry" "github.com/ethereum-optimism/optimism/op-chain-ops/script" @@ -34,6 +35,40 @@ func Init(ctx context.Context, env *Env, artifactsFS foundry.StatDirFs, intent * } } + if intent.OPCMAddress != (common.Address{}) { + env.Logger.Info("using provided OPCM address, populating state", "address", intent.OPCMAddress.Hex()) + + if intent.ContractsRelease == "dev" { + env.Logger.Warn("using dev release with existing OPCM, this field will be ignored") + } + + opcmContract := opcm.NewContract(intent.OPCMAddress, env.L1Client) + protocolVersions, err := opcmContract.ProtocolVersions(ctx) + if err != nil { + return fmt.Errorf("error getting protocol versions address: %w", err) + } + superchainConfig, err := opcmContract.SuperchainConfig(ctx) + if err != nil { + return fmt.Errorf("error getting superchain config address: %w", err) + } + env.Logger.Debug( + "populating protocol versions and superchain config addresses", + "protocolVersions", protocolVersions.Hex(), + "superchainConfig", superchainConfig.Hex(), + ) + + // The below fields are the only ones required to perform an OP Chain + // deployment via an existing OPCM contract. All the others are used + // for deploying the OPCM itself, which isn't necessary in this case. + st.SuperchainDeployment = &state.SuperchainDeployment{ + ProtocolVersionsProxyAddress: protocolVersions, + SuperchainConfigProxyAddress: superchainConfig, + } + st.ImplementationsDeployment = &state.ImplementationsDeployment{ + OpcmProxyAddress: intent.OPCMAddress, + } + } + // If the state has never been applied, we don't need to perform // any additional checks. if st.AppliedIntent == nil { diff --git a/op-chain-ops/deployer/pipeline/opchain.go b/op-chain-ops/deployer/pipeline/opchain.go index 1ae37970d7d13..27919fb8b1357 100644 --- a/op-chain-ops/deployer/pipeline/opchain.go +++ b/op-chain-ops/deployer/pipeline/opchain.go @@ -5,6 +5,8 @@ import ( "fmt" "math/big" + "github.com/ethereum-optimism/optimism/op-chain-ops/deployer/broadcaster" + "github.com/ethereum-optimism/optimism/op-chain-ops/deployer/opcm" "github.com/ethereum-optimism/optimism/op-chain-ops/deployer/state" "github.com/ethereum-optimism/optimism/op-chain-ops/foundry" @@ -27,45 +29,73 @@ func DeployOPChain(ctx context.Context, env *Env, artifactsFS foundry.StatDirFs, return fmt.Errorf("failed to get chain intent: %w", err) } + input := opcm.DeployOPChainInput{ + OpChainProxyAdminOwner: thisIntent.Roles.ProxyAdminOwner, + SystemConfigOwner: thisIntent.Roles.SystemConfigOwner, + Batcher: thisIntent.Roles.Batcher, + UnsafeBlockSigner: thisIntent.Roles.UnsafeBlockSigner, + Proposer: thisIntent.Roles.Proposer, + Challenger: thisIntent.Roles.Challenger, + BasefeeScalar: 1368, + BlobBaseFeeScalar: 801949, + L2ChainId: chainID.Big(), + OpcmProxy: st.ImplementationsDeployment.OpcmProxyAddress, + } + var dco opcm.DeployOPChainOutput - err = CallScriptBroadcast( - ctx, - CallScriptBroadcastOpts{ - L1ChainID: big.NewInt(int64(intent.L1ChainID)), - Logger: lgr, - ArtifactsFS: artifactsFS, - Deployer: env.Deployer, - Signer: env.Signer, - Client: env.L1Client, - Broadcaster: KeyedBroadcaster, - Handler: func(host *script.Host) error { - host.ImportState(st.ImplementationsDeployment.StateDump) - dco, err = opcm.DeployOPChain( - host, - opcm.DeployOPChainInput{ - OpChainProxyAdminOwner: thisIntent.Roles.ProxyAdminOwner, - SystemConfigOwner: thisIntent.Roles.SystemConfigOwner, - Batcher: thisIntent.Roles.Batcher, - UnsafeBlockSigner: thisIntent.Roles.UnsafeBlockSigner, - Proposer: thisIntent.Roles.Proposer, - Challenger: thisIntent.Roles.Challenger, - BasefeeScalar: 1368, - BlobBaseFeeScalar: 801949, - L2ChainId: chainID.Big(), - OpcmProxy: st.ImplementationsDeployment.OpcmProxyAddress, - }, - ) - return err + if intent.OPCMAddress == (common.Address{}) { + err = CallScriptBroadcast( + ctx, + CallScriptBroadcastOpts{ + L1ChainID: big.NewInt(int64(intent.L1ChainID)), + Logger: lgr, + ArtifactsFS: artifactsFS, + Deployer: env.Deployer, + Signer: env.Signer, + Client: env.L1Client, + Broadcaster: KeyedBroadcaster, + Handler: func(host *script.Host) error { + host.ImportState(st.ImplementationsDeployment.StateDump) + + dco, err = opcm.DeployOPChain( + host, + input, + ) + return err + }, }, - }, - ) - if err != nil { - return fmt.Errorf("error deploying OP chain: %w", err) + ) + if err != nil { + return fmt.Errorf("error deploying OP chain: %w", err) + } + } else { + lgr.Info("deploying using existing OPCM", "address", intent.OPCMAddress.Hex()) + + bcaster, err := broadcaster.NewKeyedBroadcaster(broadcaster.KeyedBroadcasterOpts{ + Logger: lgr, + ChainID: big.NewInt(int64(intent.L1ChainID)), + Client: env.L1Client, + Signer: env.Signer, + From: env.Deployer, + }) + if err != nil { + return fmt.Errorf("failed to create broadcaster: %w", err) + } + dco, err = opcm.DeployOPChainRaw( + ctx, + env.L1Client, + bcaster, + env.Deployer, + artifactsFS, + input, + ) + if err != nil { + return fmt.Errorf("error deploying OP chain: %w", err) + } } st.Chains = append(st.Chains, &state.ChainState{ - ID: chainID, - + ID: chainID, ProxyAdminAddress: dco.OpChainProxyAdmin, AddressManagerAddress: dco.AddressManager, L1ERC721BridgeProxyAddress: dco.L1ERC721BridgeProxy, diff --git a/op-chain-ops/deployer/state/intent.go b/op-chain-ops/deployer/state/intent.go index 17bedacd77b5d..755ad6bbba541 100644 --- a/op-chain-ops/deployer/state/intent.go +++ b/op-chain-ops/deployer/state/intent.go @@ -3,6 +3,7 @@ package state import ( "fmt" "math/big" + "strings" "github.com/ethereum-optimism/optimism/op-service/ioutil" "github.com/ethereum-optimism/optimism/op-service/jsonutil" @@ -26,6 +27,8 @@ type Intent struct { ContractsRelease string `json:"contractsVersion" toml:"contractsVersion"` + OPCMAddress common.Address `json:"opcmAddress" toml:"opcmAddress"` + Chains []*ChainIntent `json:"chains" toml:"chains"` GlobalDeployOverrides map[string]any `json:"globalDeployOverrides" toml:"globalDeployOverrides"` @@ -60,6 +63,10 @@ func (c *Intent) Check() error { return fmt.Errorf("contractArtifactsURL must be set") } + if c.ContractsRelease != "dev" && !strings.HasPrefix(c.ContractsRelease, "op-contracts/") { + return fmt.Errorf("contractsVersion must be either the literal \"dev\" or start with \"op-contracts/\"") + } + return nil } diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index b68db55580f2d..b9962af979ccf 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -32,8 +32,8 @@ "sourceCodeHash": "0xde4df0f9633dc0cdb1c9f634003ea5b0f7c5c1aebc407bc1b2f44c0ecf938649" }, "src/L1/OPContractsManager.sol": { - "initCodeHash": "0x92c72b75206e756742df25d67d295e4479e65db1473948b8f53cb4ca642025d5", - "sourceCodeHash": "0x5e04124ee67298d2f1245139baf7de79dee421d2c031c6e5abe0cd3b1bdbdb32" + "initCodeHash": "0x7903f225091334a1910470bb1b5c111f13f6f2572faf03e0c74ad625e4c0d6f5", + "sourceCodeHash": "0x3a25b0ac70b1d434773c86f46b1f2a995722e33d3273762fd5abbb541bffa7db" }, "src/L1/OptimismPortal.sol": { "initCodeHash": "0xbe2c0c81b3459014f287d8c89cdc0d27dde5d1f44e5d024fa1e4773ddc47c190", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json index ca2f2ab8ac837..57900b34e8e3c 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json @@ -15,6 +15,19 @@ "stateMutability": "nonpayable", "type": "constructor" }, + { + "inputs": [], + "name": "OUTPUT_VERSION", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "blueprints", @@ -448,6 +461,12 @@ { "anonymous": false, "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "outputVersion", + "type": "uint256" + }, { "indexed": true, "internalType": "uint256", @@ -456,9 +475,15 @@ }, { "indexed": true, - "internalType": "contract SystemConfig", - "name": "systemConfig", + "internalType": "address", + "name": "deployer", "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "deployOutput", + "type": "bytes" } ], "name": "Deployed", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInterop.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInterop.json index ca2f2ab8ac837..57900b34e8e3c 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInterop.json @@ -15,6 +15,19 @@ "stateMutability": "nonpayable", "type": "constructor" }, + { + "inputs": [], + "name": "OUTPUT_VERSION", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "blueprints", @@ -448,6 +461,12 @@ { "anonymous": false, "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "outputVersion", + "type": "uint256" + }, { "indexed": true, "internalType": "uint256", @@ -456,9 +475,15 @@ }, { "indexed": true, - "internalType": "contract SystemConfig", - "name": "systemConfig", + "internalType": "address", + "name": "deployer", "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "deployOutput", + "type": "bytes" } ], "name": "Deployed", diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 4f36897d637ca..d05ba7c8821a7 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -124,8 +124,12 @@ contract OPContractsManager is ISemver, Initializable { // -------- Constants and Variables -------- - /// @custom:semver 1.0.0-beta.6 - string public constant version = "1.0.0-beta.6"; + /// @custom:semver 1.0.0-beta.7 + string public constant version = "1.0.0-beta.7"; + + /// @notice Represents the interface version so consumers know how to decode the DeployOutput struct + /// that's emitted in the `Deployed` event. Whenever that struct changes, a new version should be used. + uint256 public constant OUTPUT_VERSION = 0; /// @notice Address of the SuperchainConfig contract shared by all chains. SuperchainConfig public immutable superchainConfig; @@ -155,9 +159,13 @@ contract OPContractsManager is ISemver, Initializable { // -------- Events -------- /// @notice Emitted when a new OP Stack chain is deployed. - /// @param l2ChainId The chain ID of the new chain. - /// @param systemConfig The address of the new chain's SystemConfig contract. - event Deployed(uint256 indexed l2ChainId, SystemConfig indexed systemConfig); + /// @param outputVersion Version that indicates how to decode the `deployOutput` argument. + /// @param l2ChainId Chain ID of the new chain. + /// @param deployer Address that deployed the chain. + /// @param deployOutput ABI-encoded output of the deployment. + event Deployed( + uint256 indexed outputVersion, uint256 indexed l2ChainId, address indexed deployer, bytes deployOutput + ); // -------- Errors -------- @@ -334,7 +342,7 @@ contract OPContractsManager is ISemver, Initializable { // Transfer ownership of the ProxyAdmin from this contract to the specified owner. output.opChainProxyAdmin.transferOwnership(_input.roles.opChainProxyAdminOwner); - emit Deployed(l2ChainId, output.systemConfigProxy); + emit Deployed(OUTPUT_VERSION, l2ChainId, msg.sender, abi.encode(output)); return output; } diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 7f52b702dd716..54c87616e1762 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -34,7 +34,9 @@ contract OPContractsManager_Harness is OPContractsManager { contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase { using stdStorage for StdStorage; - event Deployed(uint256 indexed l2ChainId, SystemConfig indexed systemConfig); + event Deployed( + uint256 indexed outputVersion, uint256 indexed l2ChainId, address indexed deployer, bytes deployOutput + ); function setUp() public override { DeployOPChain_TestBase.setUp(); @@ -86,8 +88,8 @@ contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase { } function test_deploy_succeeds() public { - vm.expectEmit(true, false, true, true); // TODO precompute the system config address. - emit Deployed(doi.l2ChainId(), SystemConfig(address(1))); + vm.expectEmit(true, true, true, false); // TODO precompute the expected `deployOutput`. + emit Deployed(0, doi.l2ChainId(), address(this), bytes("")); opcm.deploy(toOPCMDeployInput(doi)); } } diff --git a/packages/contracts-bedrock/test/Specs.t.sol b/packages/contracts-bedrock/test/Specs.t.sol index 7f13a88002529..b95604135eb01 100644 --- a/packages/contracts-bedrock/test/Specs.t.sol +++ b/packages/contracts-bedrock/test/Specs.t.sol @@ -843,6 +843,7 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OPContractsManager", _sel: _getSel("latestRelease()") }); _addSpec({ _name: "OPContractsManager", _sel: _getSel("implementations(string,string)") }); _addSpec({ _name: "OPContractsManager", _sel: _getSel("systemConfigs(uint256)") }); + _addSpec({ _name: "OPContractsManager", _sel: _getSel("OUTPUT_VERSION()") }); _addSpec({ _name: "OPContractsManager", _sel: OPContractsManager.initialize.selector }); _addSpec({ _name: "OPContractsManager", _sel: OPContractsManager.deploy.selector }); _addSpec({ _name: "OPContractsManager", _sel: OPContractsManager.blueprints.selector }); @@ -855,6 +856,7 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OPContractsManagerInterop", _sel: _getSel("latestRelease()") }); _addSpec({ _name: "OPContractsManagerInterop", _sel: _getSel("implementations(string,string)") }); _addSpec({ _name: "OPContractsManagerInterop", _sel: _getSel("systemConfigs(uint256)") }); + _addSpec({ _name: "OPContractsManagerInterop", _sel: _getSel("OUTPUT_VERSION()") }); _addSpec({ _name: "OPContractsManagerInterop", _sel: OPContractsManager.initialize.selector }); _addSpec({ _name: "OPContractsManagerInterop", _sel: OPContractsManager.deploy.selector }); _addSpec({ _name: "OPContractsManagerInterop", _sel: OPContractsManager.blueprints.selector });