diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index f316c3c8c1c13..b24b661beac89 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -51,7 +51,6 @@ import ( "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/optimism/op-core/predeploys" "github.com/ethereum-optimism/optimism/op-service/testlog" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -935,24 +934,12 @@ func runEndToEndBootstrapAndApplyUpgradeTest(t *testing.T, afactsFS foundry.Stat // Then test upgrade on the V2-deployed chain t.Run("upgrade chain v2", func(t *testing.T) { - // ABI-encode game args for FaultDisputeGameConfig{absolutePrestate} - bytes32Type := deployer.Bytes32Type - addressType := deployer.AddressType - // FaultDisputeGameConfig just needs absolutePrestate (bytes32) testPrestate := common.Hash{'P', 'R', 'E', 'S', 'T', 'A', 'T', 'E'} - cannonArgs, err := abi.Arguments{{Type: bytes32Type}}.Pack(testPrestate) - require.NoError(t, err) // PermissionedDisputeGameConfig needs absolutePrestate, proposer, challenger testProposer := common.Address{'P'} testChallenger := common.Address{'C'} - permissionedArgs, err := abi.Arguments{ - {Type: bytes32Type}, - {Type: addressType}, - {Type: addressType}, - }.Pack(testPrestate, testProposer, testChallenger) - require.NoError(t, err) upgradeConfig := embedded.UpgradeOPChainInput{ Prank: superchainProxyAdminOwner, @@ -964,19 +951,24 @@ func runEndToEndBootstrapAndApplyUpgradeTest(t *testing.T, afactsFS foundry.Stat Enabled: true, InitBond: big.NewInt(1000000000000000000), GameType: embedded.GameTypeCannon, - GameArgs: cannonArgs, + FaultDisputeGameConfig: &embedded.FaultDisputeGameConfig{ + AbsolutePrestate: testPrestate, + }, }, { Enabled: true, InitBond: big.NewInt(1000000000000000000), GameType: embedded.GameTypePermissionedCannon, - GameArgs: permissionedArgs, + PermissionedDisputeGameConfig: &embedded.PermissionedDisputeGameConfig{ + AbsolutePrestate: testPrestate, + Proposer: testProposer, + Challenger: testChallenger, + }, }, { Enabled: false, InitBond: big.NewInt(0), GameType: embedded.GameTypeCannonKona, - GameArgs: []byte{}, // Disabled games don't need args }, }, ExtraInstructions: []embedded.ExtraInstruction{ diff --git a/op-deployer/pkg/deployer/integration_test/cli/manage_add_game_type_v2_test.go b/op-deployer/pkg/deployer/integration_test/cli/manage_add_game_type_v2_test.go index 3b9444b3a9c6b..ae2ca691f2fc6 100644 --- a/op-deployer/pkg/deployer/integration_test/cli/manage_add_game_type_v2_test.go +++ b/op-deployer/pkg/deployer/integration_test/cli/manage_add_game_type_v2_test.go @@ -22,7 +22,6 @@ import ( "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-service/testutils" "github.com/ethereum-optimism/optimism/op-service/testutils/devnet" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/require" @@ -134,23 +133,12 @@ func TestManageAddGameTypeV2_Integration(t *testing.T) { _, afactsFS := testutil.LocalArtifacts(t) shared.RunPastUpgradesWithRPC(t, runner.l1RPC, afactsFS, lgr, 11155111, l1ProxyAdminOwner, systemConfigProxy) - bytes32Type := deployer.Bytes32Type - addressType := deployer.AddressType - // FaultDisputeGameConfig just needs absolutePrestate (bytes32) testPrestate := common.Hash{'P', 'R', 'E', 'S', 'T', 'A', 'T', 'E'} - cannonArgs, err := abi.Arguments{{Type: bytes32Type}}.Pack(testPrestate) - require.NoError(t, err) // PermissionedDisputeGameConfig needs absolutePrestate, proposer, challenger testProposer := common.Address{'P'} testChallenger := common.Address{'C'} - permissionedArgs, err := abi.Arguments{ - {Type: bytes32Type}, - {Type: addressType}, - {Type: addressType}, - }.Pack(testPrestate, testProposer, testChallenger) - require.NoError(t, err) testConfig := embedded.UpgradeOPChainInput{ Prank: l1ProxyAdminOwner, @@ -162,19 +150,24 @@ func TestManageAddGameTypeV2_Integration(t *testing.T) { Enabled: true, InitBond: big.NewInt(1000000000000000000), GameType: embedded.GameTypeCannon, - GameArgs: cannonArgs, + FaultDisputeGameConfig: &embedded.FaultDisputeGameConfig{ + AbsolutePrestate: testPrestate, + }, }, { Enabled: true, InitBond: big.NewInt(1000000000000000000), GameType: embedded.GameTypePermissionedCannon, - GameArgs: permissionedArgs, + PermissionedDisputeGameConfig: &embedded.PermissionedDisputeGameConfig{ + AbsolutePrestate: testPrestate, + Proposer: testProposer, + Challenger: testChallenger, + }, }, { Enabled: false, InitBond: big.NewInt(0), GameType: embedded.GameTypeCannonKona, - GameArgs: []byte{}, // Disabled games don't need args }, }, ExtraInstructions: []embedded.ExtraInstruction{ diff --git a/op-deployer/pkg/deployer/integration_test/shared/shared.go b/op-deployer/pkg/deployer/integration_test/shared/shared.go index fe240b601628f..31c27275f0b11 100644 --- a/op-deployer/pkg/deployer/integration_test/shared/shared.go +++ b/op-deployer/pkg/deployer/integration_test/shared/shared.go @@ -17,7 +17,6 @@ import ( "github.com/ethereum-optimism/optimism/op-chain-ops/foundry" "github.com/ethereum-optimism/optimism/op-chain-ops/opcmregistry" "github.com/ethereum-optimism/optimism/op-chain-ops/script" - "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" @@ -25,7 +24,6 @@ import ( "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/upgrade/embedded" "github.com/ethereum-optimism/optimism/op-deployer/pkg/env" "github.com/ethereum/go-ethereum" - "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/core/types" @@ -255,37 +253,32 @@ func buildV2OPCMUpgradeConfig(t *testing.T, prank, opcmAddr, systemConfigProxy c // Build dispute game configs with dummy prestates // CANNON and PERMISSIONED_CANNON are the standard game types - cannonArgs, err := abi.Arguments{{Type: deployer.Bytes32Type}}.Pack(opcmregistry.DummyCannonPrestate) - require.NoError(t, err) - - permissionedArgs, err := abi.Arguments{ - {Type: deployer.Bytes32Type}, - {Type: deployer.AddressType}, - {Type: deployer.AddressType}, - }.Pack(opcmregistry.DummyCannonPrestate, common.Address{}, common.Address{}) - require.NoError(t, err) - - cannonKonaArgs, err := abi.Arguments{{Type: deployer.Bytes32Type}}.Pack(opcmregistry.DummyCannonKonaPrestate) - require.NoError(t, err) - disputeGameConfigs := []embedded.DisputeGameConfig{ { Enabled: true, InitBond: big.NewInt(0), GameType: embedded.GameTypeCannon, - GameArgs: cannonArgs, + FaultDisputeGameConfig: &embedded.FaultDisputeGameConfig{ + AbsolutePrestate: opcmregistry.DummyCannonPrestate, + }, }, { Enabled: true, InitBond: big.NewInt(0), GameType: embedded.GameTypePermissionedCannon, - GameArgs: permissionedArgs, + PermissionedDisputeGameConfig: &embedded.PermissionedDisputeGameConfig{ + AbsolutePrestate: opcmregistry.DummyCannonPrestate, + Proposer: common.Address{}, + Challenger: common.Address{}, + }, }, { Enabled: true, InitBond: big.NewInt(0), GameType: embedded.GameTypeCannonKona, - GameArgs: cannonKonaArgs, + FaultDisputeGameConfig: &embedded.FaultDisputeGameConfig{ + AbsolutePrestate: opcmregistry.DummyCannonKonaPrestate, + }, }, } diff --git a/op-deployer/pkg/deployer/manage/migrate_test.go b/op-deployer/pkg/deployer/manage/migrate_test.go index d47fa767f012b..b6fab04f5fe8a 100644 --- a/op-deployer/pkg/deployer/manage/migrate_test.go +++ b/op-deployer/pkg/deployer/manage/migrate_test.go @@ -562,25 +562,13 @@ func upgradeChainV1(t *testing.T, host *script.Host, proxyAdminOwner common.Addr // Upgrades a chain via OPCM V2 to ensure the OptimismPortal is upgraded to OptimismPortalInterop. func upgradeChainV2(t *testing.T, host *script.Host, proxyAdminOwner common.Address, systemConfigProxy common.Address, opcm common.Address) { // ABI-encode game args for FaultDisputeGameConfig{absolutePrestate} - bytes32Type, err := abi.NewType("bytes32", "", nil) - require.NoError(t, err) - addressType, err := abi.NewType("address", "", nil) - require.NoError(t, err) // FaultDisputeGameConfig just needs absolutePrestate (bytes32) testPrestate := common.Hash{'P', 'R', 'E', 'S', 'T', 'A', 'T', 'E'} - cannonArgs, err := abi.Arguments{{Type: bytes32Type}}.Pack(testPrestate) - require.NoError(t, err) // PermissionedDisputeGameConfig needs absolutePrestate, proposer, challenger testProposer := common.Address{'P'} testChallenger := common.Address{'C'} - permissionedArgs, err := abi.Arguments{ - {Type: bytes32Type}, - {Type: addressType}, - {Type: addressType}, - }.Pack(testPrestate, testProposer, testChallenger) - require.NoError(t, err) upgradeConfig := embedded.UpgradeOPChainInput{ Prank: proxyAdminOwner, @@ -592,19 +580,25 @@ func upgradeChainV2(t *testing.T, host *script.Host, proxyAdminOwner common.Addr Enabled: true, InitBond: big.NewInt(1000000000000000000), GameType: embedded.GameTypeCannon, - GameArgs: cannonArgs, + FaultDisputeGameConfig: &embedded.FaultDisputeGameConfig{ + AbsolutePrestate: testPrestate, + }, }, { Enabled: true, InitBond: big.NewInt(1000000000000000000), GameType: embedded.GameTypePermissionedCannon, - GameArgs: permissionedArgs, + PermissionedDisputeGameConfig: &embedded.PermissionedDisputeGameConfig{ + AbsolutePrestate: testPrestate, + Proposer: testProposer, + Challenger: testChallenger, + }, }, { Enabled: false, InitBond: big.NewInt(0), GameType: embedded.GameTypeCannonKona, - GameArgs: []byte{}, // Disabled games don't need args + // Disabled games don't need args }, }, ExtraInstructions: []embedded.ExtraInstruction{ diff --git a/op-deployer/pkg/deployer/upgrade/embedded/upgrade.go b/op-deployer/pkg/deployer/upgrade/embedded/upgrade.go index 37afe38a65a13..edc429b9d6e08 100644 --- a/op-deployer/pkg/deployer/upgrade/embedded/upgrade.go +++ b/op-deployer/pkg/deployer/upgrade/embedded/upgrade.go @@ -12,6 +12,33 @@ import ( "github.com/lmittmann/w3" ) +// GameType represents the type of dispute game. +type GameType uint32 + +const ( + GameTypeCannon GameType = 0 + GameTypePermissionedCannon GameType = 1 + GameTypeSuperCannon GameType = 4 + GameTypeSuperPermCannon GameType = 5 + GameTypeCannonKona GameType = 8 + GameTypeSuperCannonKona GameType = 9 +) + +var ( + // This is used to encode the fault dispute game config for the upgrade input + faultEncoder = w3.MustNewFunc("dummy((bytes32 absolutePrestate))", "") + + // 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 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))", + "") + + // This is used to encode the OP Chain config for the upgrade input + opChainConfigEncoder = w3.MustNewFunc("dummy((address systemConfigProxy,bytes32 cannonPrestate,bytes32 cannonKonaPrestate)[])", "") +) + // ScriptInput represents the input struct that is actually passed to the script. // It contains the prank, opcm, and upgrade input. type ScriptInput struct { @@ -29,6 +56,13 @@ type UpgradeOPChainInput struct { UpgradeInputV2 *UpgradeInputV2 `json:"upgradeInput,omitempty"` } +// OPChainConfig represents the configuration for an OP Chain upgrade on OPCM v1. +type OPChainConfig struct { + SystemConfigProxy common.Address `json:"systemConfigProxy"` + CannonPrestate common.Hash `json:"cannonPrestate"` + CannonKonaPrestate common.Hash `json:"cannonKonaPrestate"` +} + // UpgradeInputV2 represents the new upgrade input in OPCM v2. type UpgradeInputV2 struct { SystemConfig common.Address `json:"systemConfig"` @@ -38,10 +72,11 @@ type UpgradeInputV2 struct { // DisputeGameConfig represents the configuration for a dispute game. type DisputeGameConfig struct { - Enabled bool `json:"enabled"` - InitBond *big.Int `json:"initBond"` - GameType GameType `json:"gameType"` - GameArgs []byte `json:"gameArgs"` + Enabled bool `json:"enabled"` + InitBond *big.Int `json:"initBond"` + GameType GameType `json:"gameType"` + FaultDisputeGameConfig *FaultDisputeGameConfig `json:"faultDisputeGameConfig,omitempty"` + PermissionedDisputeGameConfig *PermissionedDisputeGameConfig `json:"permissionedDisputeGameConfig,omitempty"` } // ExtraInstruction represents an additional upgrade instruction for the upgrade on OPCM v2. @@ -50,30 +85,36 @@ type ExtraInstruction struct { Data []byte `json:"data"` } -// GameType represents the type of dispute game. -type GameType uint32 - -const ( - GameTypeCannon GameType = 0 - GameTypePermissionedCannon GameType = 1 - GameTypeSuperCannon GameType = 4 - GameTypeSuperPermCannon GameType = 5 - GameTypeCannonKona GameType = 8 - GameTypeSuperCannonKona GameType = 9 -) +// FaultDisputeGameConfig represents the configuration for a fault dispute game. +// It contains the absolute prestate of the fault dispute game. +type FaultDisputeGameConfig struct { + AbsolutePrestate common.Hash `json:"absolutePrestate"` +} -// OPChainConfig represents the configuration for an OP Chain upgrade on OPCM v1. -type OPChainConfig struct { - SystemConfigProxy common.Address `json:"systemConfigProxy"` - CannonPrestate common.Hash `json:"cannonPrestate"` - CannonKonaPrestate common.Hash `json:"cannonKonaPrestate"` +// PermissionedDisputeGameConfig represents the configuration for a permissioned dispute game. +// It contains the absolute prestate, proposer, and challenger of the permissioned dispute game. +type PermissionedDisputeGameConfig struct { + AbsolutePrestate common.Hash `json:"absolutePrestate"` + Proposer common.Address `json:"proposer"` + Challenger common.Address `json:"challenger"` } -var upgradeInputEncoder = w3.MustNewFunc("dummy((address systemConfig,(bool enabled,uint256 initBond,uint32 gameType,bytes gameArgs)[] disputeGameConfigs,(string key,bytes data)[] extraInstructions))", - "") +// EncodableUpgradeInput is an intermediate struct that matches the encoder expectation for the UpgradeInputV2 struct. +type EncodableUpgradeInput struct { + SystemConfig common.Address + DisputeGameConfigs []EncodableDisputeGameConfig + ExtraInstructions []ExtraInstruction +} -var opChainConfigEncoder = w3.MustNewFunc("dummy((address systemConfigProxy,bytes32 cannonPrestate,bytes32 cannonKonaPrestate)[])", "") +// EncodableDisputeGameConfig is an intermediate struct that matches the encoder expectation. +type EncodableDisputeGameConfig struct { + Enabled bool + InitBond *big.Int + GameType uint32 + GameArgs []byte +} +// EncodedOpChainConfigs encodes the OP Chain configs for the upgrade input, assumes is not nil func (u *UpgradeOPChainInput) EncodedOpChainConfigs() ([]byte, error) { data, err := opChainConfigEncoder.EncodeArgs(u.ChainConfigs) if err != nil { @@ -82,8 +123,67 @@ func (u *UpgradeOPChainInput) EncodedOpChainConfigs() ([]byte, error) { return data[4:], nil } +// EncodedUpgradeInputV2 encodes the upgrade input for the upgrade input, assumes is not nil func (u *UpgradeOPChainInput) EncodedUpgradeInputV2() ([]byte, error) { - data, err := upgradeInputEncoder.EncodeArgs(u.UpgradeInputV2) + + encodableConfigs := make([]EncodableDisputeGameConfig, len(u.UpgradeInputV2.DisputeGameConfigs)) + + // Validate and encode each game config. + // We iterate over the game configs in the upgrade input config and encode them into the encodable configs. + // We return an error if a game config is not valid. + for i, gameConfig := range u.UpgradeInputV2.DisputeGameConfigs { + var gameArgs []byte + var err error + + if gameConfig.Enabled { + switch gameConfig.GameType { + case GameTypeCannon, GameTypeCannonKona: + if gameConfig.FaultDisputeGameConfig == nil { + return nil, fmt.Errorf("faultDisputeGameConfig is required for game type %d", gameConfig.GameType) + } + // Encode the fault dispute game args + gameArgs, err = faultEncoder.EncodeArgs(gameConfig.FaultDisputeGameConfig) + if err != nil { + return nil, fmt.Errorf("failed to encode fault game config: %w", err) + } + case GameTypePermissionedCannon: + if gameConfig.PermissionedDisputeGameConfig == nil { + return nil, fmt.Errorf("permissionedDisputeGameConfig is required for game type %d", gameConfig.GameType) + } + // Encode the permissioned dispute game args + gameArgs, err = permEncoder.EncodeArgs(gameConfig.PermissionedDisputeGameConfig) + if err != nil { + return nil, fmt.Errorf("failed to encode permissioned game config: %w", err) + } + default: + return nil, fmt.Errorf("invalid game type %d for opcm v2", gameConfig.GameType) + } + + // Edge case check when the encoded game args length is less than 4 + if len(gameArgs) < 4 { + return nil, fmt.Errorf("encoded game args length is less than 4 for game type %d", gameConfig.GameType) + } + + // Skip the selector bytes + gameArgs = gameArgs[4:] + } + + encodableConfigs[i] = EncodableDisputeGameConfig{ + Enabled: gameConfig.Enabled, + InitBond: gameConfig.InitBond, + GameType: uint32(gameConfig.GameType), + GameArgs: gameArgs, + } + } + + // Create encodable input + encodableInput := EncodableUpgradeInput{ + SystemConfig: u.UpgradeInputV2.SystemConfig, + DisputeGameConfigs: encodableConfigs, + ExtraInstructions: u.UpgradeInputV2.ExtraInstructions, + } + + data, err := upgradeInputEncoder.EncodeArgs(encodableInput) if err != nil { return nil, fmt.Errorf("failed to encode upgrade input: %w", err) } diff --git a/op-deployer/pkg/deployer/upgrade/embedded/upgrade_test.go b/op-deployer/pkg/deployer/upgrade/embedded/upgrade_test.go index c2db9c24269cd..cf4cdbe81ea72 100644 --- a/op-deployer/pkg/deployer/upgrade/embedded/upgrade_test.go +++ b/op-deployer/pkg/deployer/upgrade/embedded/upgrade_test.go @@ -3,6 +3,7 @@ package embedded import ( "encoding/hex" "encoding/json" + "fmt" "math/big" "testing" @@ -21,7 +22,9 @@ func TestUpgradeOPChainInput_UpgradeInputV2(t *testing.T) { Enabled: true, InitBond: big.NewInt(1000), GameType: GameTypeCannon, - GameArgs: []byte{0x01, 0x02, 0x03}, + FaultDisputeGameConfig: &FaultDisputeGameConfig{ + AbsolutePrestate: common.Hash{0x01, 0x02, 0x03}, + }, }, }, ExtraInstructions: []ExtraInstruction{ @@ -47,8 +50,8 @@ func TestUpgradeOPChainInput_UpgradeInputV2(t *testing.T) { "00000000000000000000000000000000000000000000000000000000000003e8" + // disputeGameConfigs[0].initBond (1000) "0000000000000000000000000000000000000000000000000000000000000000" + // disputeGameConfigs[0].gameType "0000000000000000000000000000000000000000000000000000000000000080" + // offset to gameArgs - "0000000000000000000000000000000000000000000000000000000000000003" + // gameArgs.length - "0102030000000000000000000000000000000000000000000000000000000000" + // gameArgs data + "0000000000000000000000000000000000000000000000000000000000000020" + // gameArgs.length (32 bytes for ABI-encoded bytes32) + "0102030000000000000000000000000000000000000000000000000000000000" + // gameArgs data (absolutePrestate as bytes32) "0000000000000000000000000000000000000000000000000000000000000001" + // extraInstructions.length "0000000000000000000000000000000000000000000000000000000000000020" + // offset to extraInstructions[0] "0000000000000000000000000000000000000000000000000000000000000040" + // offset to key @@ -139,7 +142,9 @@ func TestUpgrader_ValidationPasses(t *testing.T) { Enabled: true, InitBond: big.NewInt(1000), GameType: GameTypeCannon, - GameArgs: []byte{0x01, 0x02}, + FaultDisputeGameConfig: &FaultDisputeGameConfig{ + AbsolutePrestate: common.Hash{0x01, 0x02}, + }, }, }, }, @@ -173,7 +178,9 @@ func TestUpgrader_ValidationPasses(t *testing.T) { Enabled: true, InitBond: big.NewInt(1000), GameType: GameTypeCannon, - GameArgs: []byte{0x01, 0x02}, + FaultDisputeGameConfig: &FaultDisputeGameConfig{ + AbsolutePrestate: common.Hash{0x01, 0x02}, + }, }, }, }, @@ -206,3 +213,321 @@ func TestUpgrader_ValidationPasses(t *testing.T) { }) } } + +func TestEncodedUpgradeInputV2_GameTypeConfigValidation(t *testing.T) { + tests := []struct { + name string + gameConfig DisputeGameConfig + errorContains string + shouldPass bool + }{ + { + name: "CANNON requires FaultDisputeGameConfig", + gameConfig: DisputeGameConfig{ + Enabled: true, + InitBond: big.NewInt(1000), + GameType: GameTypeCannon, + // Missing FaultDisputeGameConfig + }, + errorContains: fmt.Sprintf("faultDisputeGameConfig is required for game type %d", GameTypeCannon), + shouldPass: false, + }, + { + name: "CANNON_KONA requires FaultDisputeGameConfig", + gameConfig: DisputeGameConfig{ + Enabled: true, + InitBond: big.NewInt(1000), + GameType: GameTypeCannonKona, + // Missing FaultDisputeGameConfig + }, + errorContains: fmt.Sprintf("faultDisputeGameConfig is required for game type %d", GameTypeCannonKona), + shouldPass: false, + }, + { + name: "PERMISSIONED_CANNON requires PermissionedDisputeGameConfig", + gameConfig: DisputeGameConfig{ + Enabled: true, + InitBond: big.NewInt(1000), + GameType: GameTypePermissionedCannon, + // Missing PermissionedDisputeGameConfig + }, + errorContains: fmt.Sprintf("permissionedDisputeGameConfig is required for game type %d", GameTypePermissionedCannon), + shouldPass: false, + }, + { + name: "invalid game type returns error", + gameConfig: DisputeGameConfig{ + Enabled: true, + InitBond: big.NewInt(1000), + GameType: GameType(99), // not a valid game type (0, 1, 8) + }, + errorContains: fmt.Sprintf("invalid game type %d for opcm v2", GameType(99)), + shouldPass: false, + }, + { + name: "CANNON with correct FaultDisputeGameConfig", + gameConfig: DisputeGameConfig{ + Enabled: true, + InitBond: big.NewInt(1000), + GameType: GameTypeCannon, + FaultDisputeGameConfig: &FaultDisputeGameConfig{ + AbsolutePrestate: common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c"), + }, + }, + shouldPass: true, + }, + { + name: "CANNON_KONA with correct FaultDisputeGameConfig", + gameConfig: DisputeGameConfig{ + Enabled: true, + InitBond: big.NewInt(1000), + GameType: GameTypeCannonKona, + FaultDisputeGameConfig: &FaultDisputeGameConfig{ + AbsolutePrestate: common.HexToHash("0x03c3ebfb8e75ee51bec0814b9eb7f2e8034df88897c232eed36ea217ff1e9f40"), + }, + }, + shouldPass: true, + }, + { + name: "PERMISSIONED_CANNON with correct PermissionedDisputeGameConfig", + gameConfig: DisputeGameConfig{ + Enabled: true, + InitBond: big.NewInt(1000), + GameType: GameTypePermissionedCannon, + PermissionedDisputeGameConfig: &PermissionedDisputeGameConfig{ + AbsolutePrestate: common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c"), + Proposer: common.HexToAddress("0x1111111111111111111111111111111111111111"), + Challenger: common.HexToAddress("0x2222222222222222222222222222222222222222"), + }, + }, + shouldPass: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input := &UpgradeOPChainInput{ + Prank: common.Address{0xaa}, + Opcm: common.Address{0xbb}, + UpgradeInputV2: &UpgradeInputV2{ + SystemConfig: common.Address{0x01}, + DisputeGameConfigs: []DisputeGameConfig{tt.gameConfig}, + }, + } + + data, err := input.EncodedUpgradeInputV2() + + if tt.shouldPass { + require.NoError(t, err, "encoding should succeed for valid config") + require.NotEmpty(t, data, "encoded data should not be empty") + } else { + require.Error(t, err, "encoding should fail for invalid config") + require.Contains(t, err.Error(), tt.errorContains, "error message should contain expected text") + } + }) + } +} + +func TestEncodedUpgradeInputV2_DisabledGames(t *testing.T) { + tests := []struct { + name string + gameConfigs []DisputeGameConfig + description string + }{ + { + name: "disabled CANNON game with empty config", + gameConfigs: []DisputeGameConfig{ + { + Enabled: false, + InitBond: big.NewInt(0), + GameType: GameTypeCannon, + // No FaultDisputeGameConfig needed when disabled + }, + }, + description: "Disabled CANNON game should encode successfully with no config", + }, + { + name: "disabled CANNON_KONA game with empty config", + gameConfigs: []DisputeGameConfig{ + { + Enabled: false, + InitBond: big.NewInt(0), + GameType: GameTypeCannonKona, + // No FaultDisputeGameConfig needed when disabled + }, + }, + description: "Disabled CANNON_KONA game should encode successfully with no config", + }, + { + name: "disabled PERMISSIONED_CANNON game with empty config", + gameConfigs: []DisputeGameConfig{ + { + Enabled: false, + InitBond: big.NewInt(0), + GameType: GameTypePermissionedCannon, + // No PermissionedDisputeGameConfig needed when disabled + }, + }, + description: "Disabled PERMISSIONED_CANNON game should encode successfully with no config", + }, + { + name: "mix of enabled and disabled games", + gameConfigs: []DisputeGameConfig{ + { + Enabled: true, + InitBond: big.NewInt(1000), + GameType: GameTypeCannon, + FaultDisputeGameConfig: &FaultDisputeGameConfig{ + AbsolutePrestate: common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c"), + }, + }, + { + Enabled: true, + InitBond: big.NewInt(500), + GameType: GameTypePermissionedCannon, + PermissionedDisputeGameConfig: &PermissionedDisputeGameConfig{ + AbsolutePrestate: common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c"), + Proposer: common.HexToAddress("0x1111111111111111111111111111111111111111"), + Challenger: common.HexToAddress("0x2222222222222222222222222222222222222222"), + }, + }, + { + Enabled: false, + InitBond: big.NewInt(0), + GameType: GameTypeCannonKona, + // No config needed when disabled + }, + }, + description: "Mix of enabled and disabled games should encode successfully", + }, + { + name: "all games disabled", + gameConfigs: []DisputeGameConfig{ + { + Enabled: false, + InitBond: big.NewInt(0), + GameType: GameTypeCannon, + }, + { + Enabled: false, + InitBond: big.NewInt(0), + GameType: GameTypePermissionedCannon, + }, + { + Enabled: false, + InitBond: big.NewInt(0), + GameType: GameTypeCannonKona, + }, + }, + description: "All games disabled should encode successfully", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input := &UpgradeOPChainInput{ + Prank: common.Address{0xaa}, + Opcm: common.Address{0xbb}, + UpgradeInputV2: &UpgradeInputV2{ + SystemConfig: common.Address{0x01}, + DisputeGameConfigs: tt.gameConfigs, + }, + } + + data, err := input.EncodedUpgradeInputV2() + require.NoError(t, err, tt.description) + require.NotEmpty(t, data, "encoded data should not be empty") + }) + } +} + +func TestEncodedUpgradeInputV2_GameArgsEncoding(t *testing.T) { + t.Run("FaultDisputeGameConfig encodes correctly", func(t *testing.T) { + absolutePrestate := common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c") + 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: GameTypeCannon, + FaultDisputeGameConfig: &FaultDisputeGameConfig{ + AbsolutePrestate: absolutePrestate, + }, + }, + }, + }, + } + + data, err := input.EncodedUpgradeInputV2() + require.NoError(t, err) + require.NotEmpty(t, data) + + expected := "0000000000000000000000000000000000000000000000000000000000000020" + // offset to tuple + "0000000000000000000000000100000000000000000000000000000000000000" + // systemConfig + "0000000000000000000000000000000000000000000000000000000000000060" + // offset to disputeGameConfigs + "0000000000000000000000000000000000000000000000000000000000000160" + // offset to extraInstructions + "0000000000000000000000000000000000000000000000000000000000000001" + // disputeGameConfigs.length + "0000000000000000000000000000000000000000000000000000000000000020" + // offset to disputeGameConfigs[0] + "0000000000000000000000000000000000000000000000000000000000000001" + // disputeGameConfigs[0].enabled + "00000000000000000000000000000000000000000000000000000000000003e8" + // disputeGameConfigs[0].initBond (1000) + "0000000000000000000000000000000000000000000000000000000000000000" + // disputeGameConfigs[0].gameType + "0000000000000000000000000000000000000000000000000000000000000080" + // offset to gameArgs + "0000000000000000000000000000000000000000000000000000000000000020" + // gameArgs.length (32 bytes) + "038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c" + // gameArgs data (absolutePrestate) + "0000000000000000000000000000000000000000000000000000000000000000" // extraInstructions.length + + require.Equal(t, expected, hex.EncodeToString(data)) + }) + + t.Run("PermissionedDisputeGameConfig encodes correctly", func(t *testing.T) { + absolutePrestate := common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c") + proposer := common.HexToAddress("0x1111111111111111111111111111111111111111") + challenger := common.HexToAddress("0x2222222222222222222222222222222222222222") + + 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: GameTypePermissionedCannon, + PermissionedDisputeGameConfig: &PermissionedDisputeGameConfig{ + AbsolutePrestate: absolutePrestate, + Proposer: proposer, + Challenger: challenger, + }, + }, + }, + }, + } + + data, err := input.EncodedUpgradeInputV2() + require.NoError(t, err) + require.NotEmpty(t, data) + + expected := "0000000000000000000000000000000000000000000000000000000000000020" + // offset to tuple + "0000000000000000000000000100000000000000000000000000000000000000" + // systemConfig + "0000000000000000000000000000000000000000000000000000000000000060" + // offset to disputeGameConfigs + "00000000000000000000000000000000000000000000000000000000000001a0" + // offset to extraInstructions + "0000000000000000000000000000000000000000000000000000000000000001" + // disputeGameConfigs.length + "0000000000000000000000000000000000000000000000000000000000000020" + // offset to disputeGameConfigs[0] + "0000000000000000000000000000000000000000000000000000000000000001" + // disputeGameConfigs[0].enabled + "00000000000000000000000000000000000000000000000000000000000003e8" + // disputeGameConfigs[0].initBond (1000) + "0000000000000000000000000000000000000000000000000000000000000001" + // disputeGameConfigs[0].gameType + "0000000000000000000000000000000000000000000000000000000000000080" + // offset to gameArgs + "0000000000000000000000000000000000000000000000000000000000000060" + // gameArgs.length (96 bytes) + "038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c" + // gameArgs data (absolutePrestate) + "0000000000000000000000001111111111111111111111111111111111111111" + // gameArgs data (proposer) + "0000000000000000000000002222222222222222222222222222222222222222" + // gameArgs data (challenger) + "0000000000000000000000000000000000000000000000000000000000000000" // extraInstructions.length + + require.Equal(t, expected, hex.EncodeToString(data)) + }) +}