diff --git a/op-acceptance-tests/tests/proofs/zk/setup_test.go b/op-acceptance-tests/tests/proofs/zk/setup_test.go new file mode 100644 index 00000000000..1af9362463b --- /dev/null +++ b/op-acceptance-tests/tests/proofs/zk/setup_test.go @@ -0,0 +1,21 @@ +package zk + +import ( + gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum-optimism/optimism/op-core/devfeatures" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" +) + +func zkOpts() []presets.Option { + return []presets.Option{ + presets.WithGameTypeAdded(gameTypes.ZKDisputeGameType), + presets.WithDeployerOptions(sysgo.WithDevFeatureEnabled(devfeatures.ZKDisputeGameFlag)), + presets.WithDeployerOptions(sysgo.WithJovianAtGenesis), + } +} + +func newSystem(t devtest.T) *presets.Minimal { + return presets.NewMinimal(t, zkOpts()...) +} diff --git a/op-acceptance-tests/tests/proofs/zk/smoke_test.go b/op-acceptance-tests/tests/proofs/zk/smoke_test.go new file mode 100644 index 00000000000..632850e8658 --- /dev/null +++ b/op-acceptance-tests/tests/proofs/zk/smoke_test.go @@ -0,0 +1,23 @@ +package zk + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" +) + +// TestSmoke verifies that op-deployer correctly deploys and registers the ZK +// dispute game when ZKDisputeGameFlag is enabled. +func TestSmoke(gt *testing.T) { + t := devtest.ParallelT(gt) + sys := newSystem(t) + require := t.Require() + + zk := sys.DisputeGameFactory().ZKGameImpl() + + require.NotEmpty(zk.Address, "ZK dispute game impl must be registered in DisputeGameFactory") + require.NotZero(zk.Args.MaxChallengeDuration, "maxChallengeDuration must be set") + require.NotZero(zk.Args.MaxProveDuration, "maxProveDuration must be set") + require.Positive(zk.Args.ChallengerBond.Sign(), "challengerBond must be non-zero") + require.Equal(sys.L2Chain.ChainID().ToBig(), zk.Args.L2ChainID, "l2ChainId must match deployed chain") +} diff --git a/op-chain-ops/addresses/contracts.go b/op-chain-ops/addresses/contracts.go index 0442c33a4fe..33b6073267f 100644 --- a/op-chain-ops/addresses/contracts.go +++ b/op-chain-ops/addresses/contracts.go @@ -46,6 +46,7 @@ type ImplementationsContracts struct { AnchorStateRegistryImpl common.Address FaultDisputeGameImpl common.Address PermissionedDisputeGameImpl common.Address + ZkDisputeGameImpl common.Address StorageSetterImpl common.Address } diff --git a/op-deployer/pkg/deployer/opcm/opchain.go b/op-deployer/pkg/deployer/opcm/opchain.go index efaf7f07cbf..8e2223f5957 100644 --- a/op-deployer/pkg/deployer/opcm/opchain.go +++ b/op-deployer/pkg/deployer/opcm/opchain.go @@ -113,6 +113,7 @@ type ReadImplementationAddressesOutput struct { PermissionedDisputeGame common.Address SuperFaultDisputeGame common.Address SuperPermissionedDisputeGame common.Address + ZkDisputeGame common.Address OpcmStandardValidator common.Address OpcmInteropMigrator common.Address } diff --git a/op-deployer/pkg/deployer/pipeline/dispute_games.go b/op-deployer/pkg/deployer/pipeline/dispute_games.go index c3732482c88..5df540da876 100644 --- a/op-deployer/pkg/deployer/pipeline/dispute_games.go +++ b/op-deployer/pkg/deployer/pipeline/dispute_games.go @@ -8,8 +8,10 @@ import ( gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/upgrade/embedded" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum/common" + "github.com/lmittmann/w3" ) func DeployAdditionalDisputeGames( @@ -121,6 +123,51 @@ func deployDisputeGame( } vmAddr = out.MipsSingleton } + case state.VMTypeZK: + zkImpl := st.ImplementationsDeployment.ZkDisputeGameImpl + if zkImpl == (common.Address{}) { + return fmt.Errorf("ZkDisputeGameImpl is not deployed; ensure ZKDisputeGameFlag is set in devFeatureBitmap") + } + if game.ZKDisputeGame == nil { + return fmt.Errorf("ZKDisputeGame params must be set when VMType is ZK") + } + if game.DisputeGameType != uint32(embedded.GameTypeZKDisputeGame) { + return fmt.Errorf("DisputeGameType must be %d for ZK dispute game, got %d", embedded.GameTypeZKDisputeGame, game.DisputeGameType) + } + zk := game.ZKDisputeGame + if zk.ChallengerBond == nil || zk.ChallengerBond.ToInt().Sign() <= 0 { + return fmt.Errorf("ZKDisputeGame.ChallengerBond must be set to a positive value") + } + challengerBond := zk.ChallengerBond.ToInt() + encoded, err := zkGameArgEncoder.EncodeArgs(&embedded.ZKDisputeGameConfig{ + AbsolutePrestate: zk.AbsolutePrestate, + Verifier: zk.Verifier, + MaxChallengeDuration: zk.MaxChallengeDuration, + MaxProveDuration: zk.MaxProveDuration, + ChallengerBond: challengerBond, + }) + if err != nil { + return fmt.Errorf("failed to encode ZK game args: %w", err) + } + zkInput := opcm.SetDisputeGameImplInput{ + Factory: thisState.OpChainContracts.DisputeGameFactoryProxy, + Impl: zkImpl, + AnchorStateRegistry: common.Address{}, + GameType: game.DisputeGameType, + GameArgs: encoded[4:], + } + if game.MakeRespected { + zkInput.AnchorStateRegistry = thisState.OpChainContracts.AnchorStateRegistryProxy + } + if err := opcm.SetDisputeGameImpl(env.L1ScriptHost, zkInput); err != nil { + return fmt.Errorf("failed to set ZK dispute game impl: %w", err) + } + thisState.AdditionalDisputeGames = append(thisState.AdditionalDisputeGames, state.AdditionalDisputeGameState{ + GameType: game.DisputeGameType, + VMType: game.VMType, + GameAddress: zkImpl, + }) + return nil default: return fmt.Errorf("unsupported VM type: %v", game.VMType) } @@ -213,6 +260,10 @@ func deployDisputeGame( return nil } +// zkGameArgEncoder encodes the ZK dispute game args for SetDisputeGameImpl. +// Mirrors the zkEncoder in upgrade/embedded/upgrade.go (same ABI signature). +var zkGameArgEncoder = w3.MustNewFunc("dummy((bytes32 absolutePrestate,address verifier,uint64 maxChallengeDuration,uint64 maxProveDuration,uint256 challengerBond))", "") + func shouldDeployAdditionalDisputeGames(thisIntent *state.ChainIntent, thisState *state.ChainState) bool { if len(thisIntent.AdditionalDisputeGames) == 0 { return false diff --git a/op-deployer/pkg/deployer/pipeline/dispute_games_test.go b/op-deployer/pkg/deployer/pipeline/dispute_games_test.go new file mode 100644 index 00000000000..8b29c966cc2 --- /dev/null +++ b/op-deployer/pkg/deployer/pipeline/dispute_games_test.go @@ -0,0 +1,185 @@ +package pipeline + +import ( + "log/slog" + "math/big" + "testing" + + "github.com/ethereum-optimism/optimism/op-chain-ops/addresses" + "github.com/ethereum-optimism/optimism/op-core/devfeatures" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/require" +) + +func TestShouldDeployAdditionalDisputeGames(t *testing.T) { + dummyGame := state.AdditionalDisputeGame{VMType: state.VMTypeCannon} + + tests := []struct { + name string + intent *state.ChainIntent + st *state.ChainState + expected bool + }{ + { + name: "no_games_in_intent", + intent: &state.ChainIntent{}, + st: &state.ChainState{}, + expected: false, + }, + { + name: "games_in_intent_empty_state", + intent: &state.ChainIntent{AdditionalDisputeGames: []state.AdditionalDisputeGame{dummyGame}}, + st: &state.ChainState{}, + expected: true, + }, + { + name: "games_in_intent_already_deployed", + intent: &state.ChainIntent{AdditionalDisputeGames: []state.AdditionalDisputeGame{dummyGame}}, + st: &state.ChainState{ + AdditionalDisputeGames: []state.AdditionalDisputeGameState{ + {GameType: 1, VMType: state.VMTypeCannon}, + }, + }, + expected: false, + }, + { + name: "zk_game_in_intent_empty_state", + intent: &state.ChainIntent{AdditionalDisputeGames: []state.AdditionalDisputeGame{{VMType: state.VMTypeZK}}}, + st: &state.ChainState{}, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := shouldDeployAdditionalDisputeGames(tt.intent, tt.st) + require.Equal(t, tt.expected, got) + }) + } +} + +func TestDeployDisputeGame_ZK_ZeroImpl(t *testing.T) { + lgr := testlog.Logger(t, slog.LevelInfo) + + env := &Env{Logger: lgr} + st := &state.State{ + ImplementationsDeployment: &addresses.ImplementationsContracts{ + ZkDisputeGameImpl: common.Address{}, // zero — flag was not active + }, + } + game := state.AdditionalDisputeGame{ + VMType: state.VMTypeZK, + ZKDisputeGame: &state.ZKDisputeGameParams{ + Verifier: common.HexToAddress("0x1111111111111111111111111111111111111111"), + AbsolutePrestate: common.HexToHash("0xdeadbeef"), + ChallengerBond: (*hexutil.Big)(big.NewInt(1e18)), + }, + } + + err := deployDisputeGame(env, st, &state.ChainIntent{}, &state.ChainState{}, game) + require.ErrorContains(t, err, "ZkDisputeGameImpl is not deployed") +} + +func TestDeployDisputeGame_ZK_NilParams(t *testing.T) { + lgr := testlog.Logger(t, slog.LevelInfo) + + env := &Env{Logger: lgr} + st := &state.State{ + ImplementationsDeployment: &addresses.ImplementationsContracts{ + ZkDisputeGameImpl: common.HexToAddress("0x2222222222222222222222222222222222222222"), + }, + } + game := state.AdditionalDisputeGame{ + VMType: state.VMTypeZK, + ZKDisputeGame: nil, // params not set + } + + err := deployDisputeGame(env, st, &state.ChainIntent{}, &state.ChainState{}, game) + require.ErrorContains(t, err, "ZKDisputeGame params must be set") +} + +func TestDeployDisputeGame_ZK_WrongDisputeGameType(t *testing.T) { + lgr := testlog.Logger(t, slog.LevelInfo) + + env := &Env{Logger: lgr} + st := &state.State{ + ImplementationsDeployment: &addresses.ImplementationsContracts{ + ZkDisputeGameImpl: common.HexToAddress("0x2222222222222222222222222222222222222222"), + }, + } + game := state.AdditionalDisputeGame{ + VMType: state.VMTypeZK, + ZKDisputeGame: &state.ZKDisputeGameParams{}, + ChainProofParams: state.ChainProofParams{DisputeGameType: 0}, // wrong — must be GameTypeZKDisputeGame (10) + } + + err := deployDisputeGame(env, st, &state.ChainIntent{}, &state.ChainState{}, game) + require.ErrorContains(t, err, "DisputeGameType must be") +} + +func TestDeployDisputeGame_ZK_NilChallengerBond(t *testing.T) { + lgr := testlog.Logger(t, slog.LevelInfo) + + env := &Env{Logger: lgr} + st := &state.State{ + ImplementationsDeployment: &addresses.ImplementationsContracts{ + ZkDisputeGameImpl: common.HexToAddress("0x2222222222222222222222222222222222222222"), + }, + } + game := state.AdditionalDisputeGame{ + VMType: state.VMTypeZK, + ZKDisputeGame: &state.ZKDisputeGameParams{ + ChallengerBond: nil, + }, + ChainProofParams: state.ChainProofParams{DisputeGameType: 10}, + } + + err := deployDisputeGame(env, st, &state.ChainIntent{}, &state.ChainState{}, game) + require.ErrorContains(t, err, "ChallengerBond must be set") +} + +func TestDeployDisputeGame_ZK_ZeroChallengerBond(t *testing.T) { + lgr := testlog.Logger(t, slog.LevelInfo) + + env := &Env{Logger: lgr} + st := &state.State{ + ImplementationsDeployment: &addresses.ImplementationsContracts{ + ZkDisputeGameImpl: common.HexToAddress("0x2222222222222222222222222222222222222222"), + }, + } + game := state.AdditionalDisputeGame{ + VMType: state.VMTypeZK, + ZKDisputeGame: &state.ZKDisputeGameParams{ + ChallengerBond: (*hexutil.Big)(big.NewInt(0)), + }, + ChainProofParams: state.ChainProofParams{DisputeGameType: 10}, + } + + err := deployDisputeGame(env, st, &state.ChainIntent{}, &state.ChainState{}, game) + require.ErrorContains(t, err, "ChallengerBond must be set") +} + +func TestDeployDisputeGame_UnsupportedVMType(t *testing.T) { + lgr := testlog.Logger(t, slog.LevelInfo) + + env := &Env{Logger: lgr} + st := &state.State{ + ImplementationsDeployment: &addresses.ImplementationsContracts{}, + } + game := state.AdditionalDisputeGame{ + VMType: state.VMType("UNSUPPORTED"), + } + + err := deployDisputeGame(env, st, &state.ChainIntent{}, &state.ChainState{}, game) + require.ErrorContains(t, err, "unsupported VM type") +} + +// TestZKDisputeGameFlag validates that devfeatures.ZKDisputeGameFlag matches the expected value. +func TestZKDisputeGameFlag(t *testing.T) { + expected := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000001000000") + require.Equal(t, expected, devfeatures.ZKDisputeGameFlag, + "devfeatures.ZKDisputeGameFlag must match the expected value") +} diff --git a/op-deployer/pkg/deployer/pipeline/implementations.go b/op-deployer/pkg/deployer/pipeline/implementations.go index 96ebb76f5d2..19567086efd 100644 --- a/op-deployer/pkg/deployer/pipeline/implementations.go +++ b/op-deployer/pkg/deployer/pipeline/implementations.go @@ -4,13 +4,13 @@ import ( "fmt" "math/big" - "github.com/ethereum-optimism/optimism/op-chain-ops/addresses" - "github.com/ethereum-optimism/optimism/op-service/jsonutil" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum-optimism/optimism/op-chain-ops/addresses" "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-service/jsonutil" ) func DeployImplementations(env *Env, intent *state.Intent, st *state.State) error { @@ -100,6 +100,7 @@ func DeployImplementations(env *Env, intent *state.Intent, st *state.State) erro AnchorStateRegistryImpl: dio.AnchorStateRegistryImpl, FaultDisputeGameImpl: dio.FaultDisputeGameImpl, PermissionedDisputeGameImpl: dio.PermissionedDisputeGameImpl, + ZkDisputeGameImpl: dio.ZkDisputeGameImpl, StorageSetterImpl: dio.StorageSetterImpl, } diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index 21c26865e3f..f1479a8affc 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -104,6 +104,7 @@ func DeployOPChain(env *Env, intent *state.Intent, st *state.State, chainID comm st.ImplementationsDeployment.PreimageOracleImpl = impls.PreimageOracleSingleton st.ImplementationsDeployment.FaultDisputeGameImpl = impls.FaultDisputeGame st.ImplementationsDeployment.PermissionedDisputeGameImpl = impls.PermissionedDisputeGame + st.ImplementationsDeployment.ZkDisputeGameImpl = impls.ZkDisputeGame st.ImplementationsDeployment.OpcmStandardValidatorImpl = impls.OpcmStandardValidator return nil diff --git a/op-deployer/pkg/deployer/state/chain_intent.go b/op-deployer/pkg/deployer/state/chain_intent.go index 367d08bbd0e..4e7fdf99d7b 100644 --- a/op-deployer/pkg/deployer/state/chain_intent.go +++ b/op-deployer/pkg/deployer/state/chain_intent.go @@ -18,6 +18,7 @@ const ( VMTypeAlphabet = "ALPHABET" VMTypeCannon = "CANNON" // Corresponds to the currently released Cannon StateVersion. See: https://github.com/ethereum-optimism/optimism/blob/4c05241bc534ae5837007c32995fc62f3dd059b6/cannon/mipsevm/versions/version.go VMTypeCannonNext = "CANNON-NEXT" // Corresponds to the next in-development Cannon StateVersion. See: https://github.com/ethereum-optimism/optimism/blob/4c05241bc534ae5837007c32995fc62f3dd059b6/cannon/mipsevm/versions/version.go + VMTypeZK = "ZK" // ZK dispute game — uses a ZK verifier instead of a MIPS VM. ) func (v VMType) MipsVersion() uint64 { @@ -46,6 +47,18 @@ type AdditionalDisputeGame struct { ChainProofParams VMType VMType MakeRespected bool + // ZKDisputeGame holds ZK-specific configuration. Only used when VMType == VMTypeZK. + ZKDisputeGame *ZKDisputeGameParams `json:"zkDisputeGame,omitempty" toml:"zkDisputeGame,omitempty"` +} + +// ZKDisputeGameParams holds the configuration for a ZK dispute game in the upgrade pipeline. +type ZKDisputeGameParams struct { + Verifier common.Address `json:"verifier" toml:"verifier"` + AbsolutePrestate common.Hash `json:"absolutePrestate" toml:"absolutePrestate"` + MaxChallengeDuration uint64 `json:"maxChallengeDuration" toml:"maxChallengeDuration"` + MaxProveDuration uint64 `json:"maxProveDuration" toml:"maxProveDuration"` + ChallengerBond *hexutil.Big `json:"challengerBond" toml:"challengerBond"` + InitBond *hexutil.Big `json:"initBond" toml:"initBond"` } type L2DevGenesisParams struct { @@ -101,6 +114,7 @@ var ErrGasLimitZeroValue = fmt.Errorf("chain has a gas limit set to zero value") var ErrNonStandardValue = fmt.Errorf("chain contains non-standard config value") var ErrEip1559ZeroValue = fmt.Errorf("eip1559 param is set to zero value") var ErrIncompatibleValue = fmt.Errorf("chain contains incompatible config value") +var ErrZKDisputeGameMissingParams = fmt.Errorf("ZK dispute game is missing required params") func (c *ChainIntent) Check() error { if c.ID == emptyHash { @@ -153,6 +167,20 @@ func (c *ChainIntent) Check() error { return c.DangerousAltDAConfig.Check(nil) } + for _, game := range c.AdditionalDisputeGames { + if game.VMType == VMTypeZK { + if game.ZKDisputeGame == nil { + return fmt.Errorf("%w: zkDisputeGame config must be set when VMType is ZK, chainId=%s", ErrZKDisputeGameMissingParams, c.ID) + } + if game.ZKDisputeGame.Verifier == (common.Address{}) { + return fmt.Errorf("%w: Verifier must not be zero address, chainId=%s", ErrZKDisputeGameMissingParams, c.ID) + } + if game.ZKDisputeGame.AbsolutePrestate == (common.Hash{}) { + return fmt.Errorf("%w: AbsolutePrestate must not be zero, chainId=%s", ErrZKDisputeGameMissingParams, c.ID) + } + } + } + return nil } diff --git a/op-deployer/pkg/deployer/state/chain_intent_test.go b/op-deployer/pkg/deployer/state/chain_intent_test.go index cd0a981c1c3..9e03a4287cb 100644 --- a/op-deployer/pkg/deployer/state/chain_intent_test.go +++ b/op-deployer/pkg/deployer/state/chain_intent_test.go @@ -9,6 +9,95 @@ import ( "github.com/stretchr/testify/require" ) +func validBaseChainIntent() *ChainIntent { + return &ChainIntent{ + ID: common.HexToHash("0x01"), + Roles: ChainRoles{ + L1ProxyAdminOwner: common.HexToAddress("0x01"), + L2ProxyAdminOwner: common.HexToAddress("0x02"), + SystemConfigOwner: common.HexToAddress("0x03"), + UnsafeBlockSigner: common.HexToAddress("0x04"), + Batcher: common.HexToAddress("0x05"), + Proposer: common.HexToAddress("0x06"), + Challenger: common.HexToAddress("0x07"), + }, + Eip1559DenominatorCanyon: 5000, + Eip1559Denominator: 5000, + Eip1559Elasticity: 5000, + GasLimit: 30_000_000, + BaseFeeVaultRecipient: common.HexToAddress("0x08"), + L1FeeVaultRecipient: common.HexToAddress("0x09"), + SequencerFeeVaultRecipient: common.HexToAddress("0x0A"), + OperatorFeeVaultRecipient: common.HexToAddress("0x0B"), + } +} + +func TestChainIntentCheck_ZKDisputeGame(t *testing.T) { + verifier := common.HexToAddress("0xabc") + prestate := common.HexToHash("0xdef") + + tests := []struct { + name string + game AdditionalDisputeGame + expectErr error + }{ + { + name: "valid ZK game passes", + game: AdditionalDisputeGame{ + VMType: VMTypeZK, + ZKDisputeGame: &ZKDisputeGameParams{ + Verifier: verifier, + AbsolutePrestate: prestate, + }, + }, + expectErr: nil, + }, + { + name: "nil ZKDisputeGame params fails", + game: AdditionalDisputeGame{ + VMType: VMTypeZK, + ZKDisputeGame: nil, + }, + expectErr: ErrZKDisputeGameMissingParams, + }, + { + name: "zero Verifier address fails", + game: AdditionalDisputeGame{ + VMType: VMTypeZK, + ZKDisputeGame: &ZKDisputeGameParams{ + Verifier: common.Address{}, + AbsolutePrestate: prestate, + }, + }, + expectErr: ErrZKDisputeGameMissingParams, + }, + { + name: "zero AbsolutePrestate fails", + game: AdditionalDisputeGame{ + VMType: VMTypeZK, + ZKDisputeGame: &ZKDisputeGameParams{ + Verifier: verifier, + AbsolutePrestate: common.Hash{}, + }, + }, + expectErr: ErrZKDisputeGameMissingParams, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := validBaseChainIntent() + c.AdditionalDisputeGames = []AdditionalDisputeGame{tt.game} + err := c.Check() + if tt.expectErr != nil { + require.ErrorIs(t, err, tt.expectErr) + } else { + require.NoError(t, err) + } + }) + } +} + func TestGetInitialLiquidity(t *testing.T) { tests := []struct { name string diff --git a/op-deployer/pkg/deployer/upgrade/embedded/upgrade.go b/op-deployer/pkg/deployer/upgrade/embedded/upgrade.go index e321d26d1bb..633bd035983 100644 --- a/op-deployer/pkg/deployer/upgrade/embedded/upgrade.go +++ b/op-deployer/pkg/deployer/upgrade/embedded/upgrade.go @@ -32,6 +32,9 @@ var ( // This is used to encode the permissioned dispute game config for the upgrade input permEncoder = w3.MustNewFunc("dummy((bytes32 absolutePrestate,address proposer,address challenger))", "") + // This is used to encode the ZK dispute game config for the upgrade input + zkEncoder = w3.MustNewFunc("dummy((bytes32 absolutePrestate,address verifier,uint64 maxChallengeDuration,uint64 maxProveDuration,uint256 challengerBond))", "") + // This is used to encode the upgrade input for the upgrade input upgradeInputEncoder = w3.MustNewFunc("dummy((address systemConfig,(bool enabled,uint256 initBond,uint32 gameType,bytes gameArgs)[] disputeGameConfigs,(string key,bytes data)[] extraInstructions))", "") @@ -66,6 +69,7 @@ type DisputeGameConfig struct { GameType GameType `json:"gameType"` FaultDisputeGameConfig *FaultDisputeGameConfig `json:"faultDisputeGameConfig,omitempty"` PermissionedDisputeGameConfig *PermissionedDisputeGameConfig `json:"permissionedDisputeGameConfig,omitempty"` + ZKDisputeGameConfig *ZKDisputeGameConfig `json:"zkDisputeGameConfig,omitempty"` } // ExtraInstruction represents an additional upgrade instruction for the upgrade on OPCM v2. @@ -88,6 +92,16 @@ type PermissionedDisputeGameConfig struct { Challenger common.Address `json:"challenger"` } +// ZKDisputeGameConfig represents the configuration for a ZK dispute game. +// It contains the absolute prestate, verifier address, challenge/prove durations, and challenger bond. +type ZKDisputeGameConfig struct { + AbsolutePrestate common.Hash `json:"absolutePrestate"` + Verifier common.Address `json:"verifier"` + MaxChallengeDuration uint64 `json:"maxChallengeDuration"` + MaxProveDuration uint64 `json:"maxProveDuration"` + ChallengerBond *big.Int `json:"challengerBond"` +} + // EncodableUpgradeInput is an intermediate struct that matches the encoder expectation for the UpgradeInputV2 struct. type EncodableUpgradeInput struct { SystemConfig common.Address @@ -135,6 +149,15 @@ func (u *UpgradeOPChainInput) EncodedUpgradeInputV2() ([]byte, error) { if err != nil { return nil, fmt.Errorf("failed to encode permissioned game config: %w", err) } + case GameTypeZKDisputeGame: + if gameConfig.ZKDisputeGameConfig == nil { + return nil, fmt.Errorf("zkDisputeGameConfig is required for game type %d", gameConfig.GameType) + } + // Encode the ZK dispute game args + gameArgs, err = zkEncoder.EncodeArgs(gameConfig.ZKDisputeGameConfig) + if err != nil { + return nil, fmt.Errorf("failed to encode ZK game config: %w", err) + } default: return nil, fmt.Errorf("invalid game type %d for opcm v2", gameConfig.GameType) } diff --git a/op-deployer/pkg/deployer/upgrade/embedded/upgrade_test.go b/op-deployer/pkg/deployer/upgrade/embedded/upgrade_test.go index 6ef8a824e3b..cfd9395addc 100644 --- a/op-deployer/pkg/deployer/upgrade/embedded/upgrade_test.go +++ b/op-deployer/pkg/deployer/upgrade/embedded/upgrade_test.go @@ -176,6 +176,17 @@ func TestEncodedUpgradeInputV2_GameTypeConfigValidation(t *testing.T) { errorContains: fmt.Sprintf("permissionedDisputeGameConfig is required for game type %d", GameTypePermissionedCannon), shouldPass: false, }, + { + name: "ZK_DISPUTE_GAME requires ZKDisputeGameConfig", + gameConfig: DisputeGameConfig{ + Enabled: true, + InitBond: big.NewInt(1000), + GameType: GameTypeZKDisputeGame, + // Missing ZKDisputeGameConfig + }, + errorContains: fmt.Sprintf("zkDisputeGameConfig is required for game type %d", GameTypeZKDisputeGame), + shouldPass: false, + }, { name: "invalid game type returns error", gameConfig: DisputeGameConfig{ @@ -224,6 +235,22 @@ func TestEncodedUpgradeInputV2_GameTypeConfigValidation(t *testing.T) { }, shouldPass: true, }, + { + name: "ZK_DISPUTE_GAME with correct ZKDisputeGameConfig", + gameConfig: DisputeGameConfig{ + Enabled: true, + InitBond: big.NewInt(1000), + GameType: GameTypeZKDisputeGame, + ZKDisputeGameConfig: &ZKDisputeGameConfig{ + AbsolutePrestate: common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c"), + Verifier: common.HexToAddress("0x3333333333333333333333333333333333333333"), + MaxChallengeDuration: 3600, + MaxProveDuration: 7200, + ChallengerBond: new(big.Int).SetUint64(1e9), + }, + }, + shouldPass: true, + }, } for _, tt := range tests { @@ -322,6 +349,18 @@ func TestEncodedUpgradeInputV2_DisabledGames(t *testing.T) { }, description: "Mix of enabled and disabled games should encode successfully", }, + { + name: "disabled ZK game with empty config", + gameConfigs: []DisputeGameConfig{ + { + Enabled: false, + InitBond: big.NewInt(0), + GameType: GameTypeZKDisputeGame, + // No ZKDisputeGameConfig needed when disabled + }, + }, + description: "Disabled ZK game should encode successfully with no config", + }, { name: "all games disabled", gameConfigs: []DisputeGameConfig{ @@ -452,4 +491,59 @@ func TestEncodedUpgradeInputV2_GameArgsEncoding(t *testing.T) { require.Equal(t, expected, hex.EncodeToString(data)) }) + + t.Run("ZKDisputeGameConfig encodes correctly", func(t *testing.T) { + absolutePrestate := common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c") + verifier := common.HexToAddress("0x3333333333333333333333333333333333333333") + // maxChallengeDuration = 3600 = 0xe10 + // maxProveDuration = 7200 = 0x1c20 + // challengerBond = 1e18 = 0xde0b6b3a7640000 + challengerBond, _ := new(big.Int).SetString("1000000000000000000", 10) + + input := &UpgradeOPChainInput{ + Prank: common.Address{0xaa}, + Opcm: common.Address{0xbb}, + UpgradeInputV2: &UpgradeInputV2{ + SystemConfig: common.Address{0x01}, + DisputeGameConfigs: []DisputeGameConfig{ + { + Enabled: true, + InitBond: big.NewInt(1000), + GameType: GameTypeZKDisputeGame, + ZKDisputeGameConfig: &ZKDisputeGameConfig{ + AbsolutePrestate: absolutePrestate, + Verifier: verifier, + MaxChallengeDuration: 3600, + MaxProveDuration: 7200, + ChallengerBond: challengerBond, + }, + }, + }, + }, + } + + data, err := input.EncodedUpgradeInputV2() + require.NoError(t, err) + require.NotEmpty(t, data) + + expected := "0000000000000000000000000000000000000000000000000000000000000020" + // offset to tuple + "0000000000000000000000000100000000000000000000000000000000000000" + // systemConfig + "0000000000000000000000000000000000000000000000000000000000000060" + // offset to disputeGameConfigs + "00000000000000000000000000000000000000000000000000000000000001e0" + // offset to extraInstructions + "0000000000000000000000000000000000000000000000000000000000000001" + // disputeGameConfigs.length + "0000000000000000000000000000000000000000000000000000000000000020" + // offset to disputeGameConfigs[0] + "0000000000000000000000000000000000000000000000000000000000000001" + // disputeGameConfigs[0].enabled + "00000000000000000000000000000000000000000000000000000000000003e8" + // disputeGameConfigs[0].initBond (1000) + "000000000000000000000000000000000000000000000000000000000000000a" + // disputeGameConfigs[0].gameType (10) + "0000000000000000000000000000000000000000000000000000000000000080" + // offset to gameArgs + "00000000000000000000000000000000000000000000000000000000000000a0" + // gameArgs.length (160 bytes) + "038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c" + // absolutePrestate + "0000000000000000000000003333333333333333333333333333333333333333" + // verifier + "0000000000000000000000000000000000000000000000000000000000000e10" + // maxChallengeDuration (3600) + "0000000000000000000000000000000000000000000000000000000000001c20" + // maxProveDuration (7200) + "0000000000000000000000000000000000000000000000000de0b6b3a7640000" + // challengerBond (1e18) + "0000000000000000000000000000000000000000000000000000000000000000" // extraInstructions.length + + require.Equal(t, expected, hex.EncodeToString(data)) + }) } diff --git a/op-devstack/dsl/proofs/zk_dispute_game.go b/op-devstack/dsl/proofs/zk_dispute_game.go new file mode 100644 index 00000000000..3922fca8e8a --- /dev/null +++ b/op-devstack/dsl/proofs/zk_dispute_game.go @@ -0,0 +1,76 @@ +package proofs + +import ( + "encoding/binary" + "math/big" + + gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum/go-ethereum/common" +) + +// zkArgsLength is the byte length of packed ZK game args as produced by +// OPContractsManagerUtils._encodeGameArgs for ZK_DISPUTE_GAME. +// +// Layout (abi.encodePacked, 172 bytes): +// +// [0-31] absolutePrestate (bytes32) +// [32-51] verifier (address) +// [52-59] maxChallengeDuration (uint64) +// [60-67] maxProveDuration (uint64) +// [68-99] challengerBond (uint256) +// [100-119] anchorStateRegistry (address) +// [120-139] weth (address) +// [140-171] l2ChainId (uint256) +const zkArgsLength = 172 + +// ZKGameArgs contains the parsed arguments for a ZK dispute game template. +type ZKGameArgs struct { + AbsolutePrestate common.Hash + Verifier common.Address + MaxChallengeDuration uint64 + MaxProveDuration uint64 + ChallengerBond *big.Int + AnchorStateRegistry common.Address + WETH common.Address + L2ChainID *big.Int +} + +// ZKDisputeGame holds the impl address and parsed args for a deployed ZK dispute game. +type ZKDisputeGame struct { + Address common.Address + Args ZKGameArgs +} + +// ZKGameImpl returns the ZK dispute game implementation address and its parsed +// constructor args from the DisputeGameFactory. +func (f *DisputeGameFactory) ZKGameImpl() *ZKDisputeGame { + impl := f.GameImpl(gameTypes.ZKDisputeGameType) + raw := f.GameArgs(gameTypes.ZKDisputeGameType) + f.require.Len(raw, zkArgsLength, "ZK game args must be exactly %d bytes", zkArgsLength) + + var prestate common.Hash + copy(prestate[:], raw[0:32]) + + var verifier common.Address + copy(verifier[:], raw[32:52]) + + var asr common.Address + copy(asr[:], raw[100:120]) + + var weth common.Address + copy(weth[:], raw[120:140]) + + return &ZKDisputeGame{ + Address: impl.Address, + Args: ZKGameArgs{ + AbsolutePrestate: prestate, + Verifier: verifier, + MaxChallengeDuration: binary.BigEndian.Uint64(raw[52:60]), + MaxProveDuration: binary.BigEndian.Uint64(raw[60:68]), + ChallengerBond: new(big.Int).SetBytes(raw[68:100]), + AnchorStateRegistry: asr, + WETH: weth, + L2ChainID: new(big.Int).SetBytes(raw[140:172]), + }, + } +} diff --git a/op-devstack/sysgo/add_game_type.go b/op-devstack/sysgo/add_game_type.go index 5aced90edbc..d23813cc94b 100644 --- a/op-devstack/sysgo/add_game_type.go +++ b/op-devstack/sysgo/add_game_type.go @@ -127,6 +127,11 @@ func addGameTypesForRuntime( cannonPrestate := PrestateForGameType(t, gameTypes.CannonGameType) cannonKonaPrestate := PrestateForGameType(t, gameTypes.CannonKonaGameType) + var zkDisputeGameConfig *embedded.ZKDisputeGameConfig + if enabled[gameTypes.ZKDisputeGameType] { + zkDisputeGameConfig = ZKDisputeGameConfigForRuntime(t) + } + configs := []embedded.DisputeGameConfig{ { Enabled: enabled[gameTypes.CannonGameType], @@ -170,9 +175,10 @@ func addGameTypesForRuntime( GameType: embedded.GameTypeSuperCannonKona, }, { - Enabled: false, - InitBond: new(big.Int), - GameType: embedded.GameTypeZKDisputeGame, + Enabled: enabled[gameTypes.ZKDisputeGameType], + InitBond: initBond, + GameType: embedded.GameTypeZKDisputeGame, + ZKDisputeGameConfig: zkDisputeGameConfig, }, } @@ -228,6 +234,18 @@ func addGameTypesForRuntime( delegateCallWithSetCode(t, l1PAOKey, client, l2Net.opcmImpl, calldata[0].Data) } +// ZKDisputeGameConfigForRuntime returns a ZKDisputeGameConfig for use in devstack/test environments. +// The verifier is set to address(0) as a placeholder; real deployments must supply a valid verifier. +func ZKDisputeGameConfigForRuntime(t devtest.CommonT) *embedded.ZKDisputeGameConfig { + return &embedded.ZKDisputeGameConfig{ + AbsolutePrestate: common.Hash{}, // placeholder for devstack + Verifier: common.Address{}, // address(0) — external verifier not yet wired + MaxChallengeDuration: 604800, // 7 days + MaxProveDuration: 259200, // 3 days + ChallengerBond: eth.GWei(80_000_000).ToBig(), + } +} + func PrestateForGameType(t devtest.CommonT, gameType gameTypes.GameType) common.Hash { switch gameType { case gameTypes.CannonGameType: diff --git a/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol b/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol index 82cdc031757..a5a1468fdd2 100644 --- a/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol @@ -38,6 +38,7 @@ contract ReadImplementationAddresses is Script { address permissionedDisputeGame; address superFaultDisputeGame; address superPermissionedDisputeGame; + address zkDisputeGame; address opcmStandardValidator; address opcmInteropMigrator; } @@ -69,6 +70,7 @@ contract ReadImplementationAddresses is Script { output_.permissionedDisputeGame = impls.permissionedDisputeGameImpl; output_.superFaultDisputeGame = impls.superFaultDisputeGameImpl; output_.superPermissionedDisputeGame = impls.superPermissionedDisputeGameImpl; + output_.zkDisputeGame = impls.zkDisputeGameImpl; // Get L1CrossDomainMessenger from AddressManager IAddressManager am = IAddressManager(_input.addressManager);