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

op-deployer: Support overriding proof parameters (#12732)

* op-deployer: Support overriding proof parameters

This PR adds support for overiding proofs params using the `GlobalDeployOverrides` and `DeployOverrides` fields. Previously, these values were hardcoded. To prevent a recursive import, I refactored the `standard.go` file into a standalone package.

Closes https://github.com/ethereum-optimism/optimism/issues/12711.

* import cycle

* add tests, ability to disable post-checks

* rename to dangerously

* op-deployer: Add support for deploying interop chains

* code review updates
parent 96ef7bb4
......@@ -5,6 +5,8 @@ import (
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
"github.com/ethereum/go-ethereum/common"
......@@ -173,7 +175,7 @@ func DeploySuperchainToL1(l1Host *script.Host, superCfg *SuperchainConfig) (*Sup
ProtocolVersionsProxy: superDeployment.ProtocolVersionsProxy,
OpcmProxyOwner: superDeployment.SuperchainProxyAdmin,
UseInterop: superCfg.Implementations.UseInterop,
StandardVersionsToml: opcm.StandardVersionsMainnetData,
StandardVersionsToml: standard.VersionsMainnetData,
})
if err != nil {
return nil, fmt.Errorf("failed to deploy Implementations contracts: %w", err)
......
......@@ -4,7 +4,7 @@ import (
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
......@@ -79,7 +79,7 @@ func (r *InteropDevRecipe) Build(addrs devkeys.Addresses) (*WorldConfig, error)
MipsVersion: big.NewInt(1),
},
UseInterop: true,
StandardVersionsToml: opcm.StandardVersionsMainnetData,
StandardVersionsToml: standard.VersionsMainnetData,
},
SuperchainL1DeployConfig: genesis.SuperchainL1DeployConfig{
RequiredProtocolVersion: params.OPStackSupport,
......
......@@ -2,12 +2,19 @@ package bootstrap
import (
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum-optimism/optimism/op-service/cliapp"
"github.com/urfave/cli/v2"
)
const (
ArtifactsLocatorFlagName = "artifacts-locator"
WithdrawalDelaySecondsFlagName = "withdrawal-delay-seconds"
MinProposalSizeBytesFlagName = "min-proposal-size-bytes"
ChallengePeriodSecondsFlagName = "challenge-period-seconds"
ProofMaturityDelaySecondsFlagName = "proof-maturity-delay-seconds"
DisputeGameFinalityDelaySecondsFlagName = "dispute-game-finality-delay-seconds"
MIPSVersionFlagName = "mips-version"
)
var (
......@@ -16,12 +23,54 @@ var (
Usage: "Locator for artifacts.",
EnvVars: deployer.PrefixEnvVar("ARTIFACTS_LOCATOR"),
}
WithdrawalDelaySecondsFlag = &cli.Uint64Flag{
Name: WithdrawalDelaySecondsFlagName,
Usage: "Withdrawal delay in seconds.",
EnvVars: deployer.PrefixEnvVar("WITHDRAWAL_DELAY_SECONDS"),
Value: standard.WithdrawalDelaySeconds,
}
MinProposalSizeBytesFlag = &cli.Uint64Flag{
Name: MinProposalSizeBytesFlagName,
Usage: "Minimum proposal size in bytes.",
EnvVars: deployer.PrefixEnvVar("MIN_PROPOSAL_SIZE_BYTES"),
Value: standard.MinProposalSizeBytes,
}
ChallengePeriodSecondsFlag = &cli.Uint64Flag{
Name: ChallengePeriodSecondsFlagName,
Usage: "Challenge period in seconds.",
EnvVars: deployer.PrefixEnvVar("CHALLENGE_PERIOD_SECONDS"),
Value: standard.ChallengePeriodSeconds,
}
ProofMaturityDelaySecondsFlag = &cli.Uint64Flag{
Name: ProofMaturityDelaySecondsFlagName,
Usage: "Proof maturity delay in seconds.",
EnvVars: deployer.PrefixEnvVar("PROOF_MATURITY_DELAY_SECONDS"),
Value: standard.ProofMaturityDelaySeconds,
}
DisputeGameFinalityDelaySecondsFlag = &cli.Uint64Flag{
Name: DisputeGameFinalityDelaySecondsFlagName,
Usage: "Dispute game finality delay in seconds.",
EnvVars: deployer.PrefixEnvVar("DISPUTE_GAME_FINALITY_DELAY_SECONDS"),
Value: standard.DisputeGameFinalityDelaySeconds,
}
MIPSVersionFlag = &cli.Uint64Flag{
Name: MIPSVersionFlagName,
Usage: "MIPS version.",
EnvVars: deployer.PrefixEnvVar("MIPS_VERSION"),
Value: standard.MIPSVersion,
}
)
var OPCMFlags = []cli.Flag{
deployer.L1RPCURLFlag,
deployer.PrivateKeyFlag,
ArtifactsLocatorFlag,
WithdrawalDelaySecondsFlag,
MinProposalSizeBytesFlag,
ChallengePeriodSecondsFlag,
ProofMaturityDelaySecondsFlag,
DisputeGameFinalityDelaySecondsFlag,
MIPSVersionFlag,
}
var Commands = []*cli.Command{
......
......@@ -8,6 +8,8 @@ import (
"math/big"
"strings"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer"
......@@ -28,6 +30,8 @@ import (
)
type OPCMConfig struct {
pipeline.SuperchainProofParams
L1RPCUrl string
PrivateKey string
Logger log.Logger
......@@ -59,6 +63,30 @@ func (c *OPCMConfig) Check() error {
return fmt.Errorf("artifacts locator must be specified")
}
if c.WithdrawalDelaySeconds == 0 {
c.WithdrawalDelaySeconds = standard.WithdrawalDelaySeconds
}
if c.MinProposalSizeBytes == 0 {
c.MinProposalSizeBytes = standard.MinProposalSizeBytes
}
if c.ChallengePeriodSeconds == 0 {
c.ChallengePeriodSeconds = standard.ChallengePeriodSeconds
}
if c.ProofMaturityDelaySeconds == 0 {
c.ProofMaturityDelaySeconds = standard.ProofMaturityDelaySeconds
}
if c.DisputeGameFinalityDelaySeconds == 0 {
c.DisputeGameFinalityDelaySeconds = standard.DisputeGameFinalityDelaySeconds
}
if c.MIPSVersion == 0 {
c.MIPSVersion = standard.MIPSVersion
}
return nil
}
......@@ -82,6 +110,14 @@ func OPCMCLI(cliCtx *cli.Context) error {
PrivateKey: privateKey,
Logger: l,
ArtifactsLocator: artifactsLocator,
SuperchainProofParams: pipeline.SuperchainProofParams{
WithdrawalDelaySeconds: cliCtx.Uint64(WithdrawalDelaySecondsFlagName),
MinProposalSizeBytes: cliCtx.Uint64(MinProposalSizeBytesFlagName),
ChallengePeriodSeconds: cliCtx.Uint64(ChallengePeriodSecondsFlagName),
ProofMaturityDelaySeconds: cliCtx.Uint64(ProofMaturityDelaySecondsFlagName),
DisputeGameFinalityDelaySeconds: cliCtx.Uint64(DisputeGameFinalityDelaySecondsFlagName),
MIPSVersion: cliCtx.Uint64(MIPSVersionFlagName),
},
})
}
......@@ -116,15 +152,15 @@ func OPCM(ctx context.Context, cfg OPCMConfig) error {
}
chainIDU64 := chainID.Uint64()
superCfg, err := opcm.SuperchainFor(chainIDU64)
superCfg, err := standard.SuperchainFor(chainIDU64)
if err != nil {
return fmt.Errorf("error getting superchain config: %w", err)
}
standardVersionsTOML, err := opcm.StandardL1VersionsDataFor(chainIDU64)
standardVersionsTOML, err := standard.L1VersionsDataFor(chainIDU64)
if err != nil {
return fmt.Errorf("error getting standard versions TOML: %w", err)
}
opcmProxyOwnerAddr, err := opcm.ManagerOwnerAddrFor(chainIDU64)
opcmProxyOwnerAddr, err := standard.ManagerOwnerAddrFor(chainIDU64)
if err != nil {
return fmt.Errorf("error getting superchain proxy admin: %w", err)
}
......@@ -192,11 +228,12 @@ func OPCM(ctx context.Context, cfg OPCMConfig) error {
host,
opcm.DeployImplementationsInput{
Salt: salt,
WithdrawalDelaySeconds: big.NewInt(604800),
MinProposalSizeBytes: big.NewInt(126000),
ChallengePeriodSeconds: big.NewInt(86400),
ProofMaturityDelaySeconds: big.NewInt(604800),
DisputeGameFinalityDelaySeconds: big.NewInt(302400),
WithdrawalDelaySeconds: new(big.Int).SetUint64(cfg.WithdrawalDelaySeconds),
MinProposalSizeBytes: new(big.Int).SetUint64(cfg.MinProposalSizeBytes),
ChallengePeriodSeconds: new(big.Int).SetUint64(cfg.ChallengePeriodSeconds),
ProofMaturityDelaySeconds: new(big.Int).SetUint64(cfg.ProofMaturityDelaySeconds),
DisputeGameFinalityDelaySeconds: new(big.Int).SetUint64(cfg.DisputeGameFinalityDelaySeconds),
MipsVersion: new(big.Int).SetUint64(cfg.MIPSVersion),
Release: release,
SuperchainConfigProxy: superchainConfigAddr,
ProtocolVersionsProxy: protocolVersionsAddr,
......
......@@ -8,6 +8,7 @@ import (
"strings"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state"
op_service "github.com/ethereum-optimism/optimism/op-service"
......
......@@ -14,6 +14,8 @@ import (
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum-optimism/optimism/op-chain-ops/script"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster"
"github.com/ethereum-optimism/optimism/op-service/testutils/anvil"
......@@ -368,10 +370,10 @@ func validateOPChainDeployment(t *testing.T, cg codeGetter, st *state.State, int
alloc := chainState.Allocs.Data.Accounts
chainIntent := intent.Chains[i]
checkImmutable(t, alloc, predeploys.BaseFeeVaultAddr, chainIntent.BaseFeeVaultRecipient)
checkImmutable(t, alloc, predeploys.L1FeeVaultAddr, chainIntent.L1FeeVaultRecipient)
checkImmutable(t, alloc, predeploys.SequencerFeeVaultAddr, chainIntent.SequencerFeeVaultRecipient)
checkImmutable(t, alloc, predeploys.OptimismMintableERC721FactoryAddr, common.BigToHash(new(big.Int).SetUint64(intent.L1ChainID)))
checkImmutableBehindProxy(t, alloc, predeploys.BaseFeeVaultAddr, chainIntent.BaseFeeVaultRecipient)
checkImmutableBehindProxy(t, alloc, predeploys.L1FeeVaultAddr, chainIntent.L1FeeVaultRecipient)
checkImmutableBehindProxy(t, alloc, predeploys.SequencerFeeVaultAddr, chainIntent.SequencerFeeVaultRecipient)
checkImmutableBehindProxy(t, alloc, predeploys.OptimismMintableERC721FactoryAddr, common.BigToHash(new(big.Int).SetUint64(intent.L1ChainID)))
// ownership slots
var addrAsSlot common.Hash
......@@ -399,8 +401,12 @@ type bytesMarshaler interface {
Bytes() []byte
}
func checkImmutable(t *testing.T, allocations types.GenesisAlloc, proxyContract common.Address, thing bytesMarshaler) {
func checkImmutableBehindProxy(t *testing.T, allocations types.GenesisAlloc, proxyContract common.Address, thing bytesMarshaler) {
implementationAddress := getEIP1967ImplementationAddress(t, allocations, proxyContract)
checkImmutable(t, allocations, implementationAddress, thing)
}
func checkImmutable(t *testing.T, allocations types.GenesisAlloc, implementationAddress common.Address, thing bytesMarshaler) {
account, ok := allocations[implementationAddress]
require.True(t, ok, "%s not found in allocations", implementationAddress)
require.NotEmpty(t, account.Code, "%s should have code", implementationAddress)
......@@ -613,6 +619,168 @@ func TestApplyGenesisStrategy(t *testing.T) {
}
}
func TestProofParamOverrides(t *testing.T) {
op_e2e.InitParallel(t)
lgr := testlog.Logger(t, slog.LevelDebug)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
depKey := new(deployerKey)
l1ChainID := big.NewInt(77799777)
dk, err := devkeys.NewMnemonicDevKeys(devkeys.TestMnemonic)
require.NoError(t, err)
l2ChainID1 := uint256.NewInt(1)
deployerAddr, err := dk.Address(depKey)
require.NoError(t, err)
loc := localArtifactsLocator(t)
env, bundle, _ := createEnv(t, ctx, lgr, nil, broadcaster.NoopBroadcaster(), deployerAddr)
intent, st := newIntent(t, l1ChainID, dk, l2ChainID1, loc, loc)
intent.Chains = append(intent.Chains, newChainIntent(t, dk, l1ChainID, l2ChainID1))
intent.DeploymentStrategy = state.DeploymentStrategyGenesis
intent.GlobalDeployOverrides = map[string]any{
"withdrawalDelaySeconds": standard.WithdrawalDelaySeconds + 1,
"minProposalSizeBytes": standard.MinProposalSizeBytes + 1,
"challengePeriodSeconds": standard.ChallengePeriodSeconds + 1,
"proofMaturityDelaySeconds": standard.ProofMaturityDelaySeconds + 1,
"disputeGameFinalityDelaySeconds": standard.DisputeGameFinalityDelaySeconds + 1,
"mipsVersion": standard.MIPSVersion + 1,
"disputeGameType": standard.DisputeGameType, // This must be set to the permissioned game
"disputeAbsolutePrestate": common.Hash{'A', 'B', 'S', 'O', 'L', 'U', 'T', 'E'},
"disputeMaxGameDepth": standard.DisputeMaxGameDepth + 1,
"disputeSplitDepth": standard.DisputeSplitDepth + 1,
"disputeClockExtension": standard.DisputeClockExtension + 1,
"disputeMaxClockDuration": standard.DisputeMaxClockDuration + 1,
"dangerouslyAllowCustomDisputeParameters": true,
}
require.NoError(t, deployer.ApplyPipeline(
ctx,
env,
bundle,
intent,
st,
))
allocs := st.L1StateDump.Data.Accounts
chainState := st.Chains[0]
uint64Caster := func(t *testing.T, val any) common.Hash {
return common.BigToHash(new(big.Int).SetUint64(val.(uint64)))
}
tests := []struct {
name string
caster func(t *testing.T, val any) common.Hash
address common.Address
}{
{
"withdrawalDelaySeconds",
uint64Caster,
st.ImplementationsDeployment.DelayedWETHImplAddress,
},
{
"minProposalSizeBytes",
uint64Caster,
st.ImplementationsDeployment.PreimageOracleSingletonAddress,
},
{
"challengePeriodSeconds",
uint64Caster,
st.ImplementationsDeployment.PreimageOracleSingletonAddress,
},
{
"proofMaturityDelaySeconds",
uint64Caster,
st.ImplementationsDeployment.OptimismPortalImplAddress,
},
{
"disputeGameFinalityDelaySeconds",
uint64Caster,
st.ImplementationsDeployment.OptimismPortalImplAddress,
},
{
"disputeAbsolutePrestate",
func(t *testing.T, val any) common.Hash {
return val.(common.Hash)
},
chainState.PermissionedDisputeGameAddress,
},
{
"disputeMaxGameDepth",
uint64Caster,
chainState.PermissionedDisputeGameAddress,
},
{
"disputeSplitDepth",
uint64Caster,
chainState.PermissionedDisputeGameAddress,
},
{
"disputeClockExtension",
uint64Caster,
chainState.PermissionedDisputeGameAddress,
},
{
"disputeMaxClockDuration",
uint64Caster,
chainState.PermissionedDisputeGameAddress,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
checkImmutable(t, allocs, tt.address, tt.caster(t, intent.GlobalDeployOverrides[tt.name]))
})
}
}
func TestInteropDeployment(t *testing.T) {
op_e2e.InitParallel(t)
lgr := testlog.Logger(t, slog.LevelDebug)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
depKey := new(deployerKey)
l1ChainID := big.NewInt(77799777)
dk, err := devkeys.NewMnemonicDevKeys(devkeys.TestMnemonic)
require.NoError(t, err)
l2ChainID1 := uint256.NewInt(1)
deployerAddr, err := dk.Address(depKey)
require.NoError(t, err)
loc := localArtifactsLocator(t)
env, bundle, _ := createEnv(t, ctx, lgr, nil, broadcaster.NoopBroadcaster(), deployerAddr)
intent, st := newIntent(t, l1ChainID, dk, l2ChainID1, loc, loc)
intent.Chains = append(intent.Chains, newChainIntent(t, dk, l1ChainID, l2ChainID1))
intent.DeploymentStrategy = state.DeploymentStrategyGenesis
intent.UseInterop = true
require.NoError(t, deployer.ApplyPipeline(
ctx,
env,
bundle,
intent,
st,
))
chainState := st.Chains[0]
depManagerSlot := common.HexToHash("0x1708e077affb93e89be2665fb0fb72581be66f84dc00d25fed755ae911905b1c")
checkImmutable(t, st.L1StateDump.Data.Accounts, st.ImplementationsDeployment.SystemConfigImplAddress, depManagerSlot)
proxyAdminOwnerHash := common.BytesToHash(intent.Chains[0].Roles.L1ProxyAdminOwner.Bytes())
checkStorageSlot(t, st.L1StateDump.Data.Accounts, chainState.SystemConfigProxyAddress, depManagerSlot, proxyAdminOwnerHash)
}
func TestInvalidL2Genesis(t *testing.T) {
op_e2e.InitParallel(t)
......
......@@ -4,6 +4,8 @@ import (
"fmt"
"net/url"
"strings"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
)
type schemeUnmarshaler func(string) (*ArtifactsLocator, error)
......@@ -14,6 +16,14 @@ var schemeUnmarshalerDispatch = map[string]schemeUnmarshaler{
"https": unmarshalURL,
}
var DefaultL1ContractsLocator = &ArtifactsLocator{
Tag: standard.DefaultL1ContractsTag,
}
var DefaultL2ContractsLocator = &ArtifactsLocator{
Tag: standard.DefaultL2ContractsTag,
}
type ArtifactsLocator struct {
URL *url.URL
Tag string
......@@ -61,7 +71,7 @@ func unmarshalTag(tag string) (*ArtifactsLocator, error) {
return nil, fmt.Errorf("invalid tag: %s", tag)
}
if _, err := StandardArtifactsURLForTag(tag); err != nil {
if _, err := standard.ArtifactsURLForTag(tag); err != nil {
return nil, err
}
......
......@@ -44,6 +44,7 @@ type DeployOPChainInput struct {
DisputeSplitDepth uint64
DisputeClockExtension uint64
DisputeMaxClockDuration uint64
AllowCustomDisputeParameters bool
}
func (input *DeployOPChainInput) InputSet() bool {
......
......@@ -15,6 +15,8 @@ import (
"strings"
"time"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
......@@ -42,7 +44,7 @@ func DownloadArtifacts(ctx context.Context, loc *opcm.ArtifactsLocator, progress
var u *url.URL
var err error
if loc.IsTag() {
u, err = opcm.StandardArtifactsURLForTag(loc.Tag)
u, err = standard.ArtifactsURLForTag(loc.Tag)
if err != nil {
return nil, nil, fmt.Errorf("failed to get standard artifacts URL for tag %s: %w", loc.Tag, err)
}
......
......@@ -4,10 +4,22 @@ import (
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum-optimism/optimism/op-service/jsonutil"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state"
)
type SuperchainProofParams struct {
WithdrawalDelaySeconds uint64 `json:"withdrawalDelaySeconds" toml:"withdrawalDelaySeconds"`
MinProposalSizeBytes uint64 `json:"minProposalSizeBytes" toml:"minProposalSizeBytes"`
ChallengePeriodSeconds uint64 `json:"challengePeriodSeconds" toml:"challengePeriodSeconds"`
ProofMaturityDelaySeconds uint64 `json:"proofMaturityDelaySeconds" toml:"proofMaturityDelaySeconds"`
DisputeGameFinalityDelaySeconds uint64 `json:"disputeGameFinalityDelaySeconds" toml:"disputeGameFinalityDelaySeconds"`
MIPSVersion uint64 `json:"mipsVersion" toml:"mipsVersion"`
}
func DeployImplementations(env *Env, intent *state.Intent, st *state.State) error {
lgr := env.Logger.New("stage", "deploy-implementations")
......@@ -22,7 +34,7 @@ func DeployImplementations(env *Env, intent *state.Intent, st *state.State) erro
var contractsRelease string
var err error
if intent.L1ContractsLocator.IsTag() && intent.DeploymentStrategy == state.DeploymentStrategyLive {
standardVersionsTOML, err = opcm.StandardL1VersionsDataFor(intent.L1ChainID)
standardVersionsTOML, err = standard.L1VersionsDataFor(intent.L1ChainID)
if err != nil {
return fmt.Errorf("error getting standard versions TOML: %w", err)
}
......@@ -31,24 +43,39 @@ func DeployImplementations(env *Env, intent *state.Intent, st *state.State) erro
contractsRelease = "dev"
}
proofParams, err := jsonutil.MergeJSON(
SuperchainProofParams{
WithdrawalDelaySeconds: standard.WithdrawalDelaySeconds,
MinProposalSizeBytes: standard.MinProposalSizeBytes,
ChallengePeriodSeconds: standard.ChallengePeriodSeconds,
ProofMaturityDelaySeconds: standard.ProofMaturityDelaySeconds,
DisputeGameFinalityDelaySeconds: standard.DisputeGameFinalityDelaySeconds,
MIPSVersion: standard.MIPSVersion,
},
intent.GlobalDeployOverrides,
)
if err != nil {
return fmt.Errorf("error merging proof params from overrides: %w", err)
}
env.L1ScriptHost.ImportState(st.L1StateDump.Data)
dio, err := opcm.DeployImplementations(
env.L1ScriptHost,
opcm.DeployImplementationsInput{
Salt: st.Create2Salt,
WithdrawalDelaySeconds: big.NewInt(604800),
MinProposalSizeBytes: big.NewInt(126000),
ChallengePeriodSeconds: big.NewInt(86400),
ProofMaturityDelaySeconds: big.NewInt(604800),
DisputeGameFinalityDelaySeconds: big.NewInt(302400),
MipsVersion: big.NewInt(1),
WithdrawalDelaySeconds: new(big.Int).SetUint64(proofParams.WithdrawalDelaySeconds),
MinProposalSizeBytes: new(big.Int).SetUint64(proofParams.MinProposalSizeBytes),
ChallengePeriodSeconds: new(big.Int).SetUint64(proofParams.ChallengePeriodSeconds),
ProofMaturityDelaySeconds: new(big.Int).SetUint64(proofParams.ProofMaturityDelaySeconds),
DisputeGameFinalityDelaySeconds: new(big.Int).SetUint64(proofParams.DisputeGameFinalityDelaySeconds),
MipsVersion: new(big.Int).SetUint64(proofParams.MIPSVersion),
Release: contractsRelease,
SuperchainConfigProxy: st.SuperchainDeployment.SuperchainConfigProxyAddress,
ProtocolVersionsProxy: st.SuperchainDeployment.ProtocolVersionsProxyAddress,
OpcmProxyOwner: st.SuperchainDeployment.ProxyAdminAddress,
StandardVersionsToml: standardVersionsTOML,
UseInterop: false,
UseInterop: intent.UseInterop,
},
)
if err != nil {
......
......@@ -5,7 +5,8 @@ import (
"crypto/rand"
"fmt"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state"
"github.com/ethereum-optimism/optimism/op-chain-ops/script"
......@@ -26,12 +27,12 @@ func InitLiveStrategy(ctx context.Context, env *Env, intent *state.Intent, st *s
}
if intent.L1ContractsLocator.IsTag() {
superCfg, err := opcm.SuperchainFor(intent.L1ChainID)
superCfg, err := standard.SuperchainFor(intent.L1ChainID)
if err != nil {
return fmt.Errorf("error getting superchain config: %w", err)
}
proxyAdmin, err := opcm.ManagerOwnerAddrFor(intent.L1ChainID)
proxyAdmin, err := standard.ManagerOwnerAddrFor(intent.L1ChainID)
if err != nil {
return fmt.Errorf("error getting superchain proxy admin address: %w", err)
}
......@@ -44,7 +45,7 @@ func InitLiveStrategy(ctx context.Context, env *Env, intent *state.Intent, st *s
SuperchainConfigProxyAddress: common.Address(*superCfg.Config.SuperchainConfigAddr),
}
opcmProxy, err := opcm.ManagerImplementationAddrFor(intent.L1ChainID)
opcmProxy, err := standard.ManagerImplementationAddrFor(intent.L1ChainID)
if err != nil {
return fmt.Errorf("error getting OPCM proxy address: %w", err)
}
......
......@@ -6,6 +6,9 @@ import (
"errors"
"fmt"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum-optimism/optimism/op-service/jsonutil"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state"
......@@ -26,7 +29,10 @@ func DeployOPChainLiveStrategy(ctx context.Context, env *Env, bundle ArtifactsBu
return fmt.Errorf("failed to get chain intent: %w", err)
}
input := makeDCI(thisIntent, chainID, st)
input, err := makeDCI(intent, thisIntent, chainID, st)
if err != nil {
return fmt.Errorf("error making deploy OP chain input: %w", err)
}
var dco opcm.DeployOPChainOutput
lgr.Info("deploying OP chain using existing OPCM", "id", chainID.Hex(), "opcmAddress", st.ImplementationsDeployment.OpcmProxyAddress.Hex())
......@@ -152,7 +158,10 @@ func DeployOPChainGenesisStrategy(env *Env, intent *state.Intent, st *state.Stat
return fmt.Errorf("failed to get chain intent: %w", err)
}
input := makeDCI(thisIntent, chainID, st)
input, err := makeDCI(intent, thisIntent, chainID, st)
if err != nil {
return fmt.Errorf("error making deploy OP chain input: %w", err)
}
env.L1ScriptHost.ImportState(st.L1StateDump.Data)
......@@ -171,7 +180,33 @@ func DeployOPChainGenesisStrategy(env *Env, intent *state.Intent, st *state.Stat
return nil
}
func makeDCI(thisIntent *state.ChainIntent, chainID common.Hash, st *state.State) opcm.DeployOPChainInput {
type ChainProofParams struct {
DisputeGameType uint32 `json:"disputeGameType" toml:"disputeGameType"`
DisputeAbsolutePrestate common.Hash `json:"disputeAbsolutePrestate" toml:"disputeAbsolutePrestate"`
DisputeMaxGameDepth uint64 `json:"disputeMaxGameDepth" toml:"disputeMaxGameDepth"`
DisputeSplitDepth uint64 `json:"disputeSplitDepth" toml:"disputeSplitDepth"`
DisputeClockExtension uint64 `json:"disputeClockExtension" toml:"disputeClockExtension"`
DisputeMaxClockDuration uint64 `json:"disputeMaxClockDuration" toml:"disputeMaxClockDuration"`
DangerouslyAllowCustomDisputeParameters bool `json:"dangerouslyAllowCustomDisputeParameters" toml:"dangerouslyAllowCustomDisputeParameters"`
}
func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common.Hash, st *state.State) (opcm.DeployOPChainInput, error) {
proofParams, err := jsonutil.MergeJSON(
ChainProofParams{
DisputeGameType: standard.DisputeGameType,
DisputeAbsolutePrestate: standard.DisputeAbsolutePrestate,
DisputeMaxGameDepth: standard.DisputeMaxGameDepth,
DisputeSplitDepth: standard.DisputeSplitDepth,
DisputeClockExtension: standard.DisputeClockExtension,
DisputeMaxClockDuration: standard.DisputeMaxClockDuration,
},
intent.GlobalDeployOverrides,
thisIntent.DeployOverrides,
)
if err != nil {
return opcm.DeployOPChainInput{}, fmt.Errorf("error merging proof params from overrides: %w", err)
}
return opcm.DeployOPChainInput{
OpChainProxyAdminOwner: thisIntent.Roles.L1ProxyAdminOwner,
SystemConfigOwner: thisIntent.Roles.SystemConfigOwner,
......@@ -179,19 +214,20 @@ func makeDCI(thisIntent *state.ChainIntent, chainID common.Hash, st *state.State
UnsafeBlockSigner: thisIntent.Roles.UnsafeBlockSigner,
Proposer: thisIntent.Roles.Proposer,
Challenger: thisIntent.Roles.Challenger,
BasefeeScalar: 1368,
BlobBaseFeeScalar: 801949,
BasefeeScalar: standard.BasefeeScalar,
BlobBaseFeeScalar: standard.BlobBaseFeeScalar,
L2ChainId: chainID.Big(),
OpcmProxy: st.ImplementationsDeployment.OpcmProxyAddress,
SaltMixer: st.Create2Salt.String(), // passing through salt generated at state initialization
GasLimit: 60_000_000,
DisputeGameType: 1, // PERMISSIONED_CANNON Game Type
DisputeAbsolutePrestate: common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c"),
DisputeMaxGameDepth: 73,
DisputeSplitDepth: 30,
DisputeClockExtension: 10800, // 3 hours (input in seconds)
DisputeMaxClockDuration: 302400, // 3.5 days (input in seconds)
}
GasLimit: standard.GasLimit,
DisputeGameType: proofParams.DisputeGameType,
DisputeAbsolutePrestate: proofParams.DisputeAbsolutePrestate,
DisputeMaxGameDepth: proofParams.DisputeMaxGameDepth,
DisputeSplitDepth: proofParams.DisputeSplitDepth,
DisputeClockExtension: proofParams.DisputeClockExtension, // 3 hours (input in seconds)
DisputeMaxClockDuration: proofParams.DisputeMaxClockDuration, // 3.5 days (input in seconds)
AllowCustomDisputeParameters: proofParams.DangerouslyAllowCustomDisputeParameters,
}, nil
}
func makeChainState(chainID common.Hash, dco opcm.DeployOPChainOutput) *state.ChainState {
......
package opcm
package standard
import (
"embed"
......@@ -11,45 +11,60 @@ import (
"github.com/ethereum/go-ethereum/common"
)
const (
GasLimit uint64 = 60_000_000
BasefeeScalar uint32 = 1368
BlobBaseFeeScalar uint32 = 801949
WithdrawalDelaySeconds uint64 = 604800
MinProposalSizeBytes uint64 = 126000
ChallengePeriodSeconds uint64 = 86400
ProofMaturityDelaySeconds uint64 = 604800
DisputeGameFinalityDelaySeconds uint64 = 302400
MIPSVersion uint64 = 1
DisputeGameType uint32 = 1 // PERMISSIONED game type
DisputeMaxGameDepth uint64 = 73
DisputeSplitDepth uint64 = 30
DisputeClockExtension uint64 = 10800
DisputeMaxClockDuration uint64 = 302400
)
var DisputeAbsolutePrestate = common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c")
//go:embed standard-versions-mainnet.toml
var StandardVersionsMainnetData string
var VersionsMainnetData string
//go:embed standard-versions-sepolia.toml
var StandardVersionsSepoliaData string
var VersionsSepoliaData string
var StandardL1VersionsSepolia StandardL1Versions
var L1VersionsSepolia L1Versions
var StandardL1VersionsMainnet StandardL1Versions
var L1VersionsMainnet L1Versions
var DefaultL1ContractsLocator = &ArtifactsLocator{
Tag: "op-contracts/v1.6.0",
}
var DefaultL1ContractsTag = "op-contracts/v1.6.0"
var DefaultL2ContractsLocator = &ArtifactsLocator{
Tag: "op-contracts/v1.7.0-beta.1+l2-contracts",
}
var DefaultL2ContractsTag = "op-contracts/v1.7.0-beta.1+l2-contracts"
type StandardL1Versions struct {
Releases map[string]StandardL1VersionsReleases `toml:"releases"`
type L1Versions struct {
Releases map[string]L1VersionsReleases `toml:"releases"`
}
type StandardL1VersionsReleases struct {
OptimismPortal StandardVersionRelease `toml:"optimism_portal"`
SystemConfig StandardVersionRelease `toml:"system_config"`
AnchorStateRegistry StandardVersionRelease `toml:"anchor_state_registry"`
DelayedWETH StandardVersionRelease `toml:"delayed_weth"`
DisputeGameFactory StandardVersionRelease `toml:"dispute_game_factory"`
FaultDisputeGame StandardVersionRelease `toml:"fault_dispute_game"`
PermissionedDisputeGame StandardVersionRelease `toml:"permissioned_dispute_game"`
MIPS StandardVersionRelease `toml:"mips"`
PreimageOracle StandardVersionRelease `toml:"preimage_oracle"`
L1CrossDomainMessenger StandardVersionRelease `toml:"l1_cross_domain_messenger"`
L1ERC721Bridge StandardVersionRelease `toml:"l1_erc721_bridge"`
L1StandardBridge StandardVersionRelease `toml:"l1_standard_bridge"`
OptimismMintableERC20Factory StandardVersionRelease `toml:"optimism_mintable_erc20_factory"`
type L1VersionsReleases struct {
OptimismPortal VersionRelease `toml:"optimism_portal"`
SystemConfig VersionRelease `toml:"system_config"`
AnchorStateRegistry VersionRelease `toml:"anchor_state_registry"`
DelayedWETH VersionRelease `toml:"delayed_weth"`
DisputeGameFactory VersionRelease `toml:"dispute_game_factory"`
FaultDisputeGame VersionRelease `toml:"fault_dispute_game"`
PermissionedDisputeGame VersionRelease `toml:"permissioned_dispute_game"`
MIPS VersionRelease `toml:"mips"`
PreimageOracle VersionRelease `toml:"preimage_oracle"`
L1CrossDomainMessenger VersionRelease `toml:"l1_cross_domain_messenger"`
L1ERC721Bridge VersionRelease `toml:"l1_erc721_bridge"`
L1StandardBridge VersionRelease `toml:"l1_standard_bridge"`
OptimismMintableERC20Factory VersionRelease `toml:"optimism_mintable_erc20_factory"`
}
type StandardVersionRelease struct {
type VersionRelease struct {
Version string `toml:"version"`
ImplementationAddress common.Address `toml:"implementation_address"`
Address common.Address `toml:"address"`
......@@ -57,25 +72,25 @@ type StandardVersionRelease struct {
var _ embed.FS
func StandardL1VersionsDataFor(chainID uint64) (string, error) {
func L1VersionsDataFor(chainID uint64) (string, error) {
switch chainID {
case 1:
return StandardVersionsMainnetData, nil
return VersionsMainnetData, nil
case 11155111:
return StandardVersionsSepoliaData, nil
return VersionsSepoliaData, nil
default:
return "", fmt.Errorf("unsupported chain ID: %d", chainID)
}
}
func StandardL1VersionsFor(chainID uint64) (StandardL1Versions, error) {
func L1VersionsFor(chainID uint64) (L1Versions, error) {
switch chainID {
case 1:
return StandardL1VersionsMainnet, nil
return L1VersionsMainnet, nil
case 11155111:
return StandardL1VersionsSepolia, nil
return L1VersionsSepolia, nil
default:
return StandardL1Versions{}, fmt.Errorf("unsupported chain ID: %d", chainID)
return L1Versions{}, fmt.Errorf("unsupported chain ID: %d", chainID)
}
}
......@@ -116,7 +131,7 @@ func ManagerOwnerAddrFor(chainID uint64) (common.Address, error) {
}
}
func StandardArtifactsURLForTag(tag string) (*url.URL, error) {
func ArtifactsURLForTag(tag string) (*url.URL, error) {
switch tag {
case "op-contracts/v1.6.0":
return url.Parse(standardArtifactsURL("ee07c78c3d8d4cd8f7a933c050f5afeebaa281b57b226cc6f092b19de2a8d61f"))
......@@ -132,13 +147,13 @@ func standardArtifactsURL(checksum string) string {
}
func init() {
StandardL1VersionsMainnet = StandardL1Versions{}
if err := toml.Unmarshal([]byte(StandardVersionsMainnetData), &StandardL1VersionsMainnet); err != nil {
L1VersionsMainnet = L1Versions{}
if err := toml.Unmarshal([]byte(VersionsMainnetData), &L1VersionsMainnet); err != nil {
panic(err)
}
StandardL1VersionsSepolia = StandardL1Versions{}
if err := toml.Unmarshal([]byte(StandardVersionsSepoliaData), &StandardL1VersionsSepolia); err != nil {
L1VersionsSepolia = L1Versions{}
if err := toml.Unmarshal([]byte(VersionsSepoliaData), &L1VersionsSepolia); err != nil {
panic(err)
}
}
package state
import (
"encoding/json"
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-service/jsonutil"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/common"
......@@ -135,7 +136,7 @@ func CombineDeployConfig(intent *Intent, chainIntent *ChainIntent, state *State,
// Apply overrides after setting the main values.
var err error
if len(intent.GlobalDeployOverrides) > 0 {
cfg, err = mergeJSON(cfg, intent.GlobalDeployOverrides)
cfg, err = jsonutil.MergeJSON(cfg, intent.GlobalDeployOverrides)
if err != nil {
return genesis.DeployConfig{}, fmt.Errorf("error merging global L2 overrides: %w", err)
......@@ -143,7 +144,7 @@ func CombineDeployConfig(intent *Intent, chainIntent *ChainIntent, state *State,
}
if len(chainIntent.DeployOverrides) > 0 {
cfg, err = mergeJSON(cfg, chainIntent.DeployOverrides)
cfg, err = jsonutil.MergeJSON(cfg, chainIntent.DeployOverrides)
if err != nil {
return genesis.DeployConfig{}, fmt.Errorf("error merging chain L2 overrides: %w", err)
}
......@@ -156,40 +157,6 @@ func CombineDeployConfig(intent *Intent, chainIntent *ChainIntent, state *State,
return cfg, nil
}
// mergeJSON merges the provided overrides into the input struct. Fields
// must be JSON-serializable for this to work. Overrides are applied in
// order of precedence - i.e., the last overrides will override keys from
// all preceding overrides.
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)
......
......@@ -4,6 +4,8 @@ import (
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
"github.com/ethereum-optimism/optimism/op-service/ioutil"
......@@ -38,6 +40,8 @@ type Intent struct {
FundDevAccounts bool `json:"fundDevAccounts" toml:"fundDevAccounts"`
UseInterop bool `json:"useInterop" toml:"useInterop"`
L1ContractsLocator *opcm.ArtifactsLocator `json:"l1ContractsLocator" toml:"l1ContractsLocator"`
L2ContractsLocator *opcm.ArtifactsLocator `json:"l2ContractsLocator" toml:"l2ContractsLocator"`
......@@ -102,7 +106,7 @@ func (c *Intent) WriteToFile(path string) error {
}
func (c *Intent) checkL1Prod() error {
versions, err := opcm.StandardL1VersionsFor(c.L1ChainID)
versions, err := standard.L1VersionsFor(c.L1ChainID)
if err != nil {
return err
}
......@@ -131,7 +135,7 @@ func (c *Intent) checkL1Dev() error {
}
func (c *Intent) checkL2Prod() error {
_, err := opcm.StandardArtifactsURLForTag(c.L2ContractsLocator.Tag)
_, err := standard.ArtifactsURLForTag(c.L2ContractsLocator.Tag)
return err
}
......
package jsonutil
import "encoding/json"
// MergeJSON merges the provided overrides into the input struct. Fields
// must be JSON-serializable for this to work. Overrides are applied in
// order of precedence - i.e., the last overrides will override keys from
// all preceding overrides.
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
}
package state
package jsonutil
import (
"testing"
......@@ -13,7 +13,7 @@ func TestMergeJSON(t *testing.T) {
C bool `json:"c"`
}
out, err := mergeJSON(
out, err := MergeJSON(
testStruct{
"hello",
42,
......
......@@ -58,6 +58,7 @@ contract DeployOPChainInput is BaseDeployIO {
uint256 internal _disputeSplitDepth;
Duration internal _disputeClockExtension;
Duration internal _disputeMaxClockDuration;
bool internal _allowCustomDisputeParameters;
function set(bytes4 _sel, address _addr) public {
require(_addr != address(0), "DeployOPChainInput: cannot set zero address");
......@@ -107,6 +108,11 @@ contract DeployOPChainInput is BaseDeployIO {
else revert("DeployImplementationsInput: unknown selector");
}
function set(bytes4 _sel, bool _value) public {
if (_sel == this.allowCustomDisputeParameters.selector) _allowCustomDisputeParameters = _value;
else revert("DeployOPChainInput: unknown selector");
}
function opChainProxyAdminOwner() public view returns (address) {
require(_opChainProxyAdminOwner != address(0), "DeployOPChainInput: not set");
return _opChainProxyAdminOwner;
......@@ -206,6 +212,10 @@ contract DeployOPChainInput is BaseDeployIO {
function disputeMaxClockDuration() public view returns (Duration) {
return _disputeMaxClockDuration;
}
function allowCustomDisputeParameters() public view returns (bool) {
return _allowCustomDisputeParameters;
}
}
contract DeployOPChainOutput is BaseDeployIO {
......@@ -457,6 +467,11 @@ contract DeployOPChain is Script {
IPermissionedDisputeGame game = _doo.permissionedDisputeGame();
require(GameType.unwrap(game.gameType()) == GameType.unwrap(GameTypes.PERMISSIONED_CANNON), "DPG-10");
if (_doi.allowCustomDisputeParameters()) {
return;
}
// This hex string is the absolutePrestate of the latest op-program release, see where the
// `EXPECTED_PRESTATE_HASH` is defined in `config.yml`.
require(
......
......@@ -59,6 +59,7 @@ contract DeployOPChainInput_Test is Test {
doi.set(doi.basefeeScalar.selector, basefeeScalar);
doi.set(doi.blobBaseFeeScalar.selector, blobBaseFeeScalar);
doi.set(doi.l2ChainId.selector, l2ChainId);
doi.set(doi.allowCustomDisputeParameters.selector, true);
(IProxy opcmProxy) = DeployUtils.buildERC1967ProxyWithImpl("opcmProxy");
doi.set(doi.opcmProxy.selector, address(opcmProxy));
......@@ -74,6 +75,7 @@ contract DeployOPChainInput_Test is Test {
assertEq(blobBaseFeeScalar, doi.blobBaseFeeScalar(), "900");
assertEq(l2ChainId, doi.l2ChainId(), "1000");
assertEq(address(opcmProxy), address(doi.opcmProxy()), "1100");
assertEq(true, doi.allowCustomDisputeParameters(), "1200");
}
function test_getters_whenNotSet_revert() public {
......@@ -531,6 +533,42 @@ contract DeployOPChain_Test is DeployOPChain_TestBase {
assertEq(address(doo.opChainProxyAdmin().addressManager()), address(doo.addressManager()), "3700");
assertEq(address(doo.opChainProxyAdmin().owner()), opChainProxyAdminOwner, "3800");
}
function test_customDisputeGame_customDisabled_reverts() public {
setDOI();
doi.set(doi.disputeSplitDepth.selector, disputeSplitDepth + 1);
vm.expectRevert("DPG-90");
deployOPChain.run(doi, doo);
}
function test_customDisputeGame_customEnabled_doesNotRevert() public {
setDOI();
doi.set(doi.allowCustomDisputeParameters.selector, true);
doi.set(doi.disputeSplitDepth.selector, disputeSplitDepth + 1);
deployOPChain.run(doi, doo);
assertEq(doo.permissionedDisputeGame().splitDepth(), disputeSplitDepth + 1);
}
function setDOI() internal {
doi.set(doi.opChainProxyAdminOwner.selector, opChainProxyAdminOwner);
doi.set(doi.systemConfigOwner.selector, systemConfigOwner);
doi.set(doi.batcher.selector, batcher);
doi.set(doi.unsafeBlockSigner.selector, unsafeBlockSigner);
doi.set(doi.proposer.selector, proposer);
doi.set(doi.challenger.selector, challenger);
doi.set(doi.basefeeScalar.selector, basefeeScalar);
doi.set(doi.blobBaseFeeScalar.selector, blobBaseFeeScalar);
doi.set(doi.l2ChainId.selector, l2ChainId);
doi.set(doi.opcmProxy.selector, address(opcm));
doi.set(doi.saltMixer.selector, saltMixer);
doi.set(doi.gasLimit.selector, gasLimit);
doi.set(doi.disputeGameType.selector, disputeGameType);
doi.set(doi.disputeAbsolutePrestate.selector, disputeAbsolutePrestate);
doi.set(doi.disputeMaxGameDepth.selector, disputeMaxGameDepth);
doi.set(doi.disputeSplitDepth.selector, disputeSplitDepth);
doi.set(doi.disputeClockExtension.selector, disputeClockExtension);
doi.set(doi.disputeMaxClockDuration.selector, disputeMaxClockDuration);
}
}
contract DeployOPChain_Test_Interop is DeployOPChain_Test {
......
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