diff --git a/go.mod b/go.mod index 7d4b85cc394..de7df2704e6 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/docker/docker v27.5.1+incompatible github.com/docker/go-connections v0.5.0 github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.4-0.20251001155152-4eb15ccedf7e - github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251104194255-7380dcd87c00 + github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251121143344-5ac16e0fbb00 github.com/ethereum/go-ethereum v1.16.3 github.com/fatih/color v1.18.0 github.com/fsnotify/fsnotify v1.9.0 diff --git a/go.sum b/go.sum index 5a49923cbac..9811125a646 100644 --- a/go.sum +++ b/go.sum @@ -240,8 +240,8 @@ github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.4-0.20251001155152-4eb15c github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.4-0.20251001155152-4eb15ccedf7e/go.mod h1:DYj7+vYJ4cIB7zera9mv4LcAynCL5u4YVfoeUu6Wa+w= github.com/ethereum-optimism/op-geth v1.101603.2-0.20251016091451-5c6d276814f2 h1:fvYTR+KOcvSDd/gJuh+ALG/Fx7Y0xU3ZaDkgT/kqVi0= github.com/ethereum-optimism/op-geth v1.101603.2-0.20251016091451-5c6d276814f2/go.mod h1:Ct2QjqZ2UKgvvgKLLYzoh/DBicJZB8DXsv45DgEjcco= -github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251104194255-7380dcd87c00 h1:eLxykgilVpGE1NTJSpUkpk/cXam6+ZkbbnCfCBShEiU= -github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251104194255-7380dcd87c00/go.mod h1:NZ816PzLU1TLv1RdAvYAb6KWOj4Zm5aInT0YpDVml2Y= +github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251121143344-5ac16e0fbb00 h1:TR5Y7B+5m63V0Dno7MHcFqv/XZByQzx/4THV1T1A7+U= +github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251121143344-5ac16e0fbb00/go.mod h1:NZ816PzLU1TLv1RdAvYAb6KWOj4Zm5aInT0YpDVml2Y= github.com/ethereum/c-kzg-4844/v2 v2.1.0 h1:gQropX9YFBhl3g4HYhwE70zq3IHFRgbbNPw0Shwzf5w= github.com/ethereum/c-kzg-4844/v2 v2.1.0/go.mod h1:TC48kOKjJKPbN7C++qIgt0TJzZ70QznYR7Ob+WXl57E= github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= diff --git a/op-deployer/pkg/deployer/integration_test/cli/upgrade_test.go b/op-deployer/pkg/deployer/integration_test/cli/upgrade_test.go index c95f3aa9acf..f39f4c952cf 100644 --- a/op-deployer/pkg/deployer/integration_test/cli/upgrade_test.go +++ b/op-deployer/pkg/deployer/integration_test/cli/upgrade_test.go @@ -54,11 +54,11 @@ func TestCLIUpgrade(t *testing.T) { version: "v4.1.0", forkBlock: 9165154, // one block past the opcm deployment block }, - //{ - //contractTag: standard.ContractsV500Tag, - //version: "v5.0.0-rc.2", - //forkBlock: 9554797, // one block past the opcm deployment block - //}, + { + contractTag: standard.ContractsV500Tag, + version: "v5.0.0", + forkBlock: 9629972, // one block past the opcm deployment block + }, } for _, tc := range testCases { diff --git a/op-deployer/pkg/deployer/opcm/contract.go b/op-deployer/pkg/deployer/opcm/contract.go index 8d02f77e8e0..b928c49e132 100644 --- a/op-deployer/pkg/deployer/opcm/contract.go +++ b/op-deployer/pkg/deployer/opcm/contract.go @@ -28,6 +28,10 @@ func (c *Contract) ProtocolVersions(ctx context.Context) (common.Address, error) return c.getAddress(ctx, "protocolVersions") } +func (c *Contract) OPCMStandardValidator(ctx context.Context) (common.Address, error) { + return c.getAddress(ctx, "opcmStandardValidator") +} + func (c *Contract) getAddress(ctx context.Context, name string) (common.Address, error) { return c.callContractMethod(ctx, name, abi.Arguments{}) } diff --git a/op-deployer/pkg/deployer/standard/standard.go b/op-deployer/pkg/deployer/standard/standard.go index 59d7f64b575..46fd5820bd7 100644 --- a/op-deployer/pkg/deployer/standard/standard.go +++ b/op-deployer/pkg/deployer/standard/standard.go @@ -43,7 +43,7 @@ const ( ContractsV300Tag = "op-contracts/v3.0.0" ContractsV400Tag = "op-contracts/v4.0.0-rc.7" ContractsV410Tag = "op-contracts/v4.1.0" - ContractsV500Tag = "op-contracts/v5.0.0-rc.2" + ContractsV500Tag = "op-contracts/v5.0.0" CurrentTag = ContractsV500Tag ) diff --git a/op-deployer/pkg/deployer/upgrade/flags.go b/op-deployer/pkg/deployer/upgrade/flags.go index 92df46c204a..b1ce072a823 100644 --- a/op-deployer/pkg/deployer/upgrade/flags.go +++ b/op-deployer/pkg/deployer/upgrade/flags.go @@ -74,8 +74,8 @@ var Commands = cli.Commands{ Action: UpgradeCLI(v410.DefaultUpgrader), }, &cli.Command{ - Name: "v5.0.0-rc.2", - Usage: "upgrades a chain to version v5.0.0-rc.2 (U17 release candidate)", + Name: "v5.0.0", + Usage: "upgrades a chain to version v5.0.0 (U17)", Flags: append([]cli.Flag{ deployer.L1RPCURLFlag, ConfigFlag, diff --git a/op-validator/cmd/main.go b/op-validator/cmd/main.go index 30ab65b9490..10767dc3cdc 100644 --- a/op-validator/cmd/main.go +++ b/op-validator/cmd/main.go @@ -36,6 +36,8 @@ func main() { versionCmd(standard.ContractsV200Tag), versionCmd(standard.ContractsV300Tag), versionCmd(standard.ContractsV400Tag), + versionCmd(standard.ContractsV410Tag), + versionCmd(standard.ContractsV500Tag), }, }, } diff --git a/op-validator/pkg/service/flags.go b/op-validator/pkg/service/flags.go index cba8fbd1764..02d6f46fb49 100644 --- a/op-validator/pkg/service/flags.go +++ b/op-validator/pkg/service/flags.go @@ -34,6 +34,11 @@ var ( Usage: "L2 chain ID", Required: true, } + ProposerFlag = &cli.StringFlag{ + Name: "proposer", + Usage: "Proposer address as hex string (required for OPCMStandardValidator)", + Required: false, + } FailOnErrorFlag = &cli.BoolFlag{ Name: "fail", Usage: "Exit with non-zero code if validation errors are found", @@ -48,6 +53,7 @@ var ValidateFlags = []cli.Flag{ ProxyAdminFlag, SystemConfigFlag, L2ChainIDFlag, + ProposerFlag, FailOnErrorFlag, } @@ -58,6 +64,7 @@ type Config struct { ProxyAdmin common.Address SystemConfig common.Address L2ChainID *big.Int + Proposer common.Address } // NewConfig creates a new Config from CLI context @@ -70,11 +77,17 @@ func NewConfig(ctx *cli.Context) (*Config, error) { return nil, fmt.Errorf("invalid L2 chain ID: %s", ctx.String(L2ChainIDFlag.Name)) } + var proposer common.Address + if proposerStr := ctx.String(ProposerFlag.Name); proposerStr != "" { + proposer = common.HexToAddress(proposerStr) + } + return &Config{ L1RPCURL: ctx.String(L1RPCURLFlag.Name), AbsolutePrestate: absolutePrestate, ProxyAdmin: proxyAdmin, SystemConfig: systemConfig, L2ChainID: l2ChainID, + Proposer: proposer, }, nil } diff --git a/op-validator/pkg/service/validate.go b/op-validator/pkg/service/validate.go index 846ee2743ff..c257dea3028 100644 --- a/op-validator/pkg/service/validate.go +++ b/op-validator/pkg/service/validate.go @@ -46,6 +46,7 @@ func Validate(ctx context.Context, lgr log.Logger, release string, cfg *Config) } var validator validations.Validator + switch release { case standard.ContractsV180Tag: validator = validations.NewV180Validator(l1Client) @@ -55,14 +56,20 @@ func Validate(ctx context.Context, lgr log.Logger, release string, cfg *Config) validator = validations.NewV300Validator(l1Client) case standard.ContractsV400Tag: validator = validations.NewV400Validator(l1Client) + case standard.ContractsV410Tag: + validator = validations.NewV410Validator(l1Client) + case standard.ContractsV500Tag: + validator = validations.NewV500Validator(l1Client) default: return nil, fmt.Errorf("invalid release: %s", release) } + lgr.Info("Using Validator", "version", release) return validator.Validate(ctx, validations.BaseValidatorInput{ ProxyAdminAddress: cfg.ProxyAdmin, SystemConfigAddress: cfg.SystemConfig, AbsolutePrestate: cfg.AbsolutePrestate, L2ChainID: cfg.L2ChainID, + Proposer: cfg.Proposer, }) } diff --git a/op-validator/pkg/validations/addresses.go b/op-validator/pkg/validations/addresses.go index 92891f60f6e..3e5de4e5dd2 100644 --- a/op-validator/pkg/validations/addresses.go +++ b/op-validator/pkg/validations/addresses.go @@ -18,6 +18,10 @@ var addresses = map[uint64]map[string]common.Address{ standard.ContractsV300Tag: common.HexToAddress("0xf989Df70FB46c581ba6157Ab335c0833bA60e1f0"), // Bootstrapped on 06/03/2025 using OP Deployer. standard.ContractsV400Tag: common.HexToAddress("0x3dfc5e44043DC5998928E0b8280136b7352d3F70"), + // Bootstrapped on 10/02/2025 using OP Deployer. + standard.ContractsV410Tag: common.HexToAddress("0x845FEF377Fa9C678B3eBe33B024678538f1215dD"), + // Bootstrapped on 10/27/2025 using OP Deployer. + standard.ContractsV500Tag: common.HexToAddress("0xDCE1A51A25dD5BF02ccB4264D039EDdF11A95b43"), }, 11155111: { // Bootstrapped on 03/02/2025 using OP Deployer. @@ -28,6 +32,10 @@ var addresses = map[uint64]map[string]common.Address{ standard.ContractsV300Tag: common.HexToAddress("0x2d56022cb84ce6b961c3b4288ca36386bcd9024c"), // Bootstrapped on 06/03/2025 using OP Deployer. standard.ContractsV400Tag: common.HexToAddress("0xA8a1529547306FEC7A32a001705160f2110451aE"), + // Bootstrapped on 10/02/2025 using OP Deployer. + standard.ContractsV410Tag: common.HexToAddress("0x7B4d2a02d5fa6C7C98D835d819956EBB876Ff439"), + // Bootstrapped on 10/27/2025 using OP Deployer. + standard.ContractsV500Tag: common.HexToAddress("0x757bFA3AAABcE60112Cee3239DCD05b5F6EFaE3A"), }, } diff --git a/op-validator/pkg/validations/addresses_test.go b/op-validator/pkg/validations/addresses_test.go index 8cd670f7342..edea32cea37 100644 --- a/op-validator/pkg/validations/addresses_test.go +++ b/op-validator/pkg/validations/addresses_test.go @@ -4,13 +4,13 @@ import ( "context" "fmt" "os" + "strings" "testing" "time" + "github.com/Masterminds/semver/v3" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" - op_e2e "github.com/ethereum-optimism/optimism/op-e2e" - "github.com/ethereum-optimism/superchain-registry/validation" "github.com/ethereum/go-ethereum/rpc" "github.com/lmittmann/w3" @@ -74,11 +74,11 @@ func TestValidatorAddress(t *testing.T) { } func TestAddressValidDeployment(t *testing.T) { - op_e2e.InitParallel(t) + t.Parallel() for _, network := range []string{"mainnet", "sepolia"} { t.Run(network, func(t *testing.T) { - op_e2e.InitParallel(t) + t.Parallel() testStandardVersionNetwork(t, network) }) } @@ -90,114 +90,174 @@ func testStandardVersionNetwork(t *testing.T, network string) { var chainID uint64 if network == "mainnet" { rpcURL = os.Getenv("MAINNET_RPC_URL") + if rpcURL == "" { + rpcURL = "https://ethereum.publicnode.com" + } stdVersDefs = validation.StandardVersionsMainnet chainID = 1 } else if network == "sepolia" { rpcURL = os.Getenv("SEPOLIA_RPC_URL") + if rpcURL == "" { + rpcURL = "https://ethereum-sepolia-rpc.publicnode.com" + } stdVersDefs = validation.StandardVersionsSepolia chainID = 11155111 } else { t.Fatalf("Invalid network: %s", network) } - require.NotEmpty(t, rpcURL, "RPC URL is empty") - contractVersions := []string{ standard.ContractsV180Tag, standard.ContractsV200Tag, standard.ContractsV300Tag, standard.ContractsV400Tag, + standard.ContractsV410Tag, + standard.ContractsV500Tag, } for _, semver := range contractVersions { - version := stdVersDefs[validation.Semver(semver)] + version, ok := stdVersDefs[validation.Semver(semver)] + require.True(t, ok, "version %s not found in registry", semver) address, err := ValidatorAddress(chainID, semver) - require.NoError(t, err) + require.NoError(t, err, "failed to get validator address for %s", semver) + require.NotEqual(t, common.Address{}, address, "validator address is zero for %s", semver) rpcClient, err := rpc.Dial(rpcURL) require.NoError(t, err) t.Run(semver, func(t *testing.T) { - testStandardVersion(t, address, rpcClient, version) + testStandardVersion(t, address, rpcClient, version, semver) }) } } -func testStandardVersion(t *testing.T, address common.Address, rpcClient *rpc.Client, version validation.VersionConfig) { - type fieldDef struct { - getter string - semver string - } - fields := []fieldDef{ - { - "systemConfigVersion", - version.SystemConfig.Version, - }, - { - "mipsVersion", - version.Mips.Version, - }, - { - "optimismPortalVersion", - version.OptimismPortal.Version, - }, - { - "anchorStateRegistryVersion", - version.AnchorStateRegistry.Version, - }, - { - "delayedWETHVersion", - version.DelayedWeth.Version, - }, - { - "disputeGameFactoryVersion", - version.DisputeGameFactory.Version, - }, - { - "preimageOracleVersion", - version.PreimageOracle.Version, - }, - { - "l1CrossDomainMessengerVersion", - version.L1CrossDomainMessenger.Version, - }, - { - "l1ERC721BridgeVersion", - version.L1ERC721Bridge.Version, - }, - { - "l1StandardBridgeVersion", - version.L1StandardBridge.Version, - }, - { - "optimismMintableERC20FactoryVersion", - version.OptimismMintableERC20Factory.Version, - }, - } - +func testStandardVersion(t *testing.T, address common.Address, rpcClient *rpc.Client, version validation.VersionConfig, semverTag string) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() w3c := w3.NewClient(rpcClient) - for _, field := range fields { - fn := w3.MustNewFunc(fmt.Sprintf("%s()", field.getter), "string") - var outBytes []byte + + // Semver tags from the registry include the "op-contracts/" prefix. + cleanTag := strings.TrimPrefix(strings.TrimPrefix(semverTag, "op-contracts/"), "v") + releaseVer, err := semver.NewVersion(cleanTag) + require.NoError(t, err) + + if releaseVer.Major() >= 5 { + // For v5.0.0+ + type implFieldDef struct { + implGetter string + semver string + } + implFields := []implFieldDef{ + {"systemConfigImpl", version.SystemConfig.Version}, + {"mipsImpl", version.Mips.Version}, + {"optimismPortalImpl", version.OptimismPortal.Version}, + {"anchorStateRegistryImpl", version.AnchorStateRegistry.Version}, + {"delayedWETHImpl", version.DelayedWeth.Version}, + {"disputeGameFactoryImpl", version.DisputeGameFactory.Version}, + {"l1CrossDomainMessengerImpl", version.L1CrossDomainMessenger.Version}, + {"l1ERC721BridgeImpl", version.L1ERC721Bridge.Version}, + {"l1StandardBridgeImpl", version.L1StandardBridge.Version}, + {"optimismMintableERC20FactoryImpl", version.OptimismMintableERC20Factory.Version}, + } + + versionFn := w3.MustNewFunc("version()", "string") + for _, field := range implFields { + implGetterFn := w3.MustNewFunc(fmt.Sprintf("%s()", field.implGetter), "address") + var implAddrBytes []byte + require.NoError( + t, + w3c.CallCtx( + ctx, + eth.Call(&w3types.Message{ + To: &address, + Func: implGetterFn, + }, nil, nil).Returns(&implAddrBytes), + ), + "failed to call %s", + field.implGetter, + ) + + var implAddr common.Address + require.NoError(t, implGetterFn.DecodeReturns(implAddrBytes, &implAddr), "failed to decode %s", field.implGetter) + require.NotEqual(t, common.Address{}, implAddr, "implementation address is zero for %s", field.implGetter) + + var versionBytes []byte + require.NoError( + t, + w3c.CallCtx( + ctx, + eth.Call(&w3types.Message{ + To: &implAddr, + Func: versionFn, + }, nil, nil).Returns(&versionBytes), + ), + "failed to call version() on %s implementation", + field.implGetter, + ) + + var outVersion string + require.NoError(t, versionFn.DecodeReturns(versionBytes, &outVersion), "failed to decode version for %s", field.implGetter) + require.Equal(t, field.semver, outVersion, "version mismatch for %s", field.implGetter) + } + + preimageOracleVersionFn := w3.MustNewFunc("preimageOracleVersion()", "string") + var preimageOracleVersionBytes []byte require.NoError( t, w3c.CallCtx( ctx, eth.Call(&w3types.Message{ To: &address, - Func: fn, - }, nil, nil).Returns(&outBytes), + Func: preimageOracleVersionFn, + }, nil, nil).Returns(&preimageOracleVersionBytes), ), - "failed to call %s", - field.getter, + "failed to call preimageOracleVersion", ) - var outVersion string - require.NoError(t, fn.DecodeReturns(outBytes, &outVersion)) - require.Equal(t, field.semver, outVersion) + var preimageOracleVersion string + require.NoError(t, preimageOracleVersionFn.DecodeReturns(preimageOracleVersionBytes, &preimageOracleVersion), "failed to decode preimageOracleVersion") + require.Equal(t, version.PreimageOracle.Version, preimageOracleVersion, "version mismatch for preimageOracleVersion") + } else { + // Older versions < v5.0.0 + type fieldDef struct { + getter string + semver string + } + fields := []fieldDef{ + {"systemConfigVersion", version.SystemConfig.Version}, + {"mipsVersion", version.Mips.Version}, + {"optimismPortalVersion", version.OptimismPortal.Version}, + {"anchorStateRegistryVersion", version.AnchorStateRegistry.Version}, + {"delayedWETHVersion", version.DelayedWeth.Version}, + {"disputeGameFactoryVersion", version.DisputeGameFactory.Version}, + {"preimageOracleVersion", version.PreimageOracle.Version}, + {"l1CrossDomainMessengerVersion", version.L1CrossDomainMessenger.Version}, + {"l1ERC721BridgeVersion", version.L1ERC721Bridge.Version}, + {"l1StandardBridgeVersion", version.L1StandardBridge.Version}, + {"optimismMintableERC20FactoryVersion", version.OptimismMintableERC20Factory.Version}, + } + + for _, field := range fields { + fn := w3.MustNewFunc(fmt.Sprintf("%s()", field.getter), "string") + var outBytes []byte + require.NoError( + t, + w3c.CallCtx( + ctx, + eth.Call(&w3types.Message{ + To: &address, + Func: fn, + }, nil, nil).Returns(&outBytes), + ), + "failed to call %s", + field.getter, + ) + + var outVersion string + require.NoError(t, fn.DecodeReturns(outBytes, &outVersion), "failed to decode response for %s", field.getter) + require.Equal(t, field.semver, outVersion, "version mismatch for %s", field.getter) + } } } diff --git a/op-validator/pkg/validations/codes.go b/op-validator/pkg/validations/codes.go index 731366ee5f7..3f9c4a3deb1 100644 --- a/op-validator/pkg/validations/codes.go +++ b/op-validator/pkg/validations/codes.go @@ -93,6 +93,8 @@ var descriptions = map[string]string{ "PDDG-100": "Permissioned dispute game max game depth not set to 73", "PDDG-110": "Permissioned dispute game max clock duration not set to 302400", "PDDG-120": "Permissioned dispute game challenger address mismatch", + "PDDG-130": "Permissioned dispute game challenger address mismatch (from game implementation)", + "PDDG-140": "Permissioned dispute game proposer address mismatch", // Permissionless Dispute Game validations "PLDG-10": "Permissionless dispute game implementation not found", @@ -142,6 +144,41 @@ var descriptions = map[string]string{ "PLDG-PIMGO-10": "Permissionless dispute game preimage oracle version mismatch", "PLDG-PIMGO-20": "Permissionless dispute game preimage oracle challenge period not set to 86400", "PLDG-PIMGO-30": "Permissionless dispute game preimage oracle min proposal size not set to 126000", + + // Custom/Override validations + "OVERRIDES-L1PAOMULTISIG": "L1 Proxy Admin Owner multisig override detected (non-standard deployment)", + "OVERRIDES-CHALLENGER": "Challenger address override detected (non-standard deployment)", + + // Custom Dispute Game validations (CKDG) + "CKDG-10": "Custom dispute game implementation not found", + "CKDG-20": "Custom dispute game version mismatch", + "CKDG-40": "Custom dispute game absolute prestate mismatch", + "CKDG-60": "Custom dispute game L2 chain ID mismatch", + "CKDG-70": "Custom dispute game L2 block number not set to 0", + "CKDG-80": "Custom dispute game clock extension not set to 10800", + "CKDG-90": "Custom dispute game split depth not set to 30", + "CKDG-100": "Custom dispute game max game depth not set to 73", + "CKDG-110": "Custom dispute game max clock duration not set to 302400", + "CKDG-120": "Custom dispute game challenger address mismatch", + "CKDG-VM-10": "Custom dispute game VM version mismatch", + "CKDG-VM-20": "Custom dispute game VM implementation address mismatch", + "CKDG-VM-30": "Custom dispute game VM address mismatch", + "CKDG-ANCHORP-10": "Custom dispute game anchor state registry version mismatch", + "CKDG-ANCHORP-20": "Custom dispute game anchor state registry implementation address mismatch", + "CKDG-ANCHORP-30": "Custom dispute game anchor state registry dispute game factory address mismatch", + "CKDG-ANCHORP-40": "Custom dispute game anchor state registry root hash mismatch", + "CKDG-ANCHORP-50": "Custom dispute game anchor state registry superchain config address mismatch", + "CKDG-ANCHORP-60": "Custom dispute game anchor state registry retirement timestamp is not set", + "CKDG-DWETH-10": "Custom dispute game delayed WETH version mismatch", + "CKDG-DWETH-20": "Custom dispute game delayed WETH implementation address mismatch", + "CKDG-DWETH-30": "Custom dispute game delayed WETH owner mismatch", + "CKDG-DWETH-40": "Custom dispute game delayed WETH delay not set to 1 week", + "CKDG-DWETH-50": "Custom dispute game delayed WETH system config address mismatch", + "CKDG-DWETH-60": "Custom dispute game delayed WETH proxy admin mismatch", + "CKDG-PIMGO-10": "Custom dispute game preimage oracle version mismatch", + "CKDG-PIMGO-20": "Custom dispute game preimage oracle challenge period not set to 86400", + "CKDG-PIMGO-30": "Custom dispute game preimage oracle min proposal size not set to 126000", + "CKDG-GARGS-10": "Custom dispute game game args mismatch", } func ErrorDescription(code string) string { diff --git a/op-validator/pkg/validations/validations.go b/op-validator/pkg/validations/validations.go index 0d670015d56..891d42f3c2c 100644 --- a/op-validator/pkg/validations/validations.go +++ b/op-validator/pkg/validations/validations.go @@ -16,8 +16,13 @@ import ( "github.com/lmittmann/w3/w3types" ) +// validateFunc is used for 1.8.0-4.1.0 validation contracts var validateFunc = w3.MustNewFunc("validate((address proxyAdminAddress,address systemConfigAddress,bytes32 absolutePrestate,uint256 chainID) input,bool allowFailure)", "string") +// validateFunc500Validator is used for 5.0.0+ validation contracts +var validateFunc500Validator = w3.MustNewFunc("validate((address sysCfg,bytes32 absolutePrestate,uint256 l2ChainID,address proposer) input,bool allowFailure)", "string") + +// validateFuncArgs is used for 1.8.0-4.1.0 validation contracts type validateFuncArgs struct { ProxyAdminAddress common.Address SystemConfigAddress common.Address @@ -25,26 +30,54 @@ type validateFuncArgs struct { ChainID *big.Int } +// validateFuncArgs500Validator is used for 5.0.0+ validation contracts +type validateFuncArgs500Validator struct { + SysCfg common.Address `w3:"sysCfg"` + AbsolutePrestate common.Hash `w3:"absolutePrestate"` + L2ChainID *big.Int `w3:"l2ChainID"` + Proposer common.Address `w3:"proposer"` +} + +// Validator is used for all validation contracts type Validator interface { Validate(ctx context.Context, input BaseValidatorInput) ([]string, error) } +// BaseValidator is used for 1.8.0-4.1.0 validation contracts type BaseValidator struct { client *rpc.Client release string } +// OPCMStandardValidator is used for 5.0.0+ validation contracts +type OPCMStandardValidator struct { + client *rpc.Client + release string +} + +// BaseValidatorInput is used for all validation contracts type BaseValidatorInput struct { ProxyAdminAddress common.Address SystemConfigAddress common.Address AbsolutePrestate common.Hash L2ChainID *big.Int + Proposer common.Address } +// newBaseValidator is used for 1.8.0-4.1.0 validation contracts func newBaseValidator(client *rpc.Client, release string) *BaseValidator { return &BaseValidator{client: client, release: release} } +// newOPCMStandardValidator is used for 5.0.0+ validation contracts +func newOPCMStandardValidator(client *rpc.Client, release string) *OPCMStandardValidator { + return &OPCMStandardValidator{ + client: client, + release: release, + } +} + +// Validate (BaseValidator) is used for 1.8.0-4.1.0 validation contracts func (v *BaseValidator) Validate(ctx context.Context, input BaseValidatorInput) ([]string, error) { l1ChainID, err := ethclient.NewClient(v.client).ChainID(ctx) if err != nil { @@ -80,7 +113,50 @@ func (v *BaseValidator) Validate(ctx context.Context, input BaseValidatorInput) if err := validateFunc.DecodeReturns(rawOutput, &output); err != nil { return nil, fmt.Errorf("failed to unmarshal output: %w", err) } - return strings.Split(output, ","), nil + return parseErrors(output), nil +} + +// Validate (OPCMStandardValidator) is used for 5.0.0+ validation contracts +func (v *OPCMStandardValidator) Validate(ctx context.Context, input BaseValidatorInput) ([]string, error) { + if input.Proposer == (common.Address{}) { + return nil, fmt.Errorf("proposer address is required for OPCM validation") + } + + l1ChainID, err := ethclient.NewClient(v.client).ChainID(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get chain ID: %w", err) + } + + validatorAddr, err := ValidatorAddress(l1ChainID.Uint64(), v.release) + if err != nil { + return nil, fmt.Errorf("failed to get validator address: %w", err) + } + + var rawOutput []byte + if err := w3.NewClient(v.client).CallCtx( + ctx, + eth.Call(&w3types.Message{ + To: &validatorAddr, + Func: validateFunc500Validator, + Args: []any{ + validateFuncArgs500Validator{ + SysCfg: input.SystemConfigAddress, + AbsolutePrestate: input.AbsolutePrestate, + L2ChainID: input.L2ChainID, + Proposer: input.Proposer, + }, + true, + }, + }, nil, nil).Returns(&rawOutput), + ); err != nil { + return nil, fmt.Errorf("failed to call validate: %w", err) + } + + var output string + if err := validateFunc500Validator.DecodeReturns(rawOutput, &output); err != nil { + return nil, fmt.Errorf("failed to unmarshal output: %w", err) + } + return parseErrors(output), nil } func NewV180Validator(client *rpc.Client) *BaseValidator { @@ -98,3 +174,26 @@ func NewV300Validator(client *rpc.Client) *BaseValidator { func NewV400Validator(client *rpc.Client) *BaseValidator { return newBaseValidator(client, standard.ContractsV400Tag) } + +func NewV410Validator(client *rpc.Client) *BaseValidator { + return newBaseValidator(client, standard.ContractsV410Tag) +} + +func NewV500Validator(client *rpc.Client) *OPCMStandardValidator { + return newOPCMStandardValidator(client, standard.ContractsV500Tag) +} + +func parseErrors(output string) []string { + if idx := strings.Index(output, ":"); idx != -1 && strings.HasPrefix(output, "Chain") { + output = output[idx+1:] + } + parts := strings.Split(output, ",") + var errors []string + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + errors = append(errors, part) + } + } + return errors +} diff --git a/op-validator/pkg/validations/validations_test.go b/op-validator/pkg/validations/validations_test.go index cd9e462fa64..bd25852f6d6 100644 --- a/op-validator/pkg/validations/validations_test.go +++ b/op-validator/pkg/validations/validations_test.go @@ -60,6 +60,12 @@ func TestValidate_Mocked(t *testing.T) { return NewV400Validator(rpcClient) }, }, + { + version: standard.ContractsV410Tag, + validator: func(rpcClient *rpc.Client) Validator { + return NewV410Validator(rpcClient) + }, + }, } for _, tt := range tests { t.Run(string(tt.version), func(t *testing.T) { @@ -89,3 +95,71 @@ func TestValidate_Mocked(t *testing.T) { }) } } + +func TestOPCMStandardValidator(t *testing.T) { + callResult := "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004b504444472d34302c504444472d44574554482d33302c504444472d414e43484f52502d34302c504c44472d34302c504c44472d44574554482d33302c504c44472d414e43484f52502d3430000000000000000000000000000000000000000000" + + tests := []struct { + name string + input BaseValidatorInput + expectError bool + errorMsg string + }{ + { + name: "successful validation", + input: BaseValidatorInput{ + SystemConfigAddress: common.HexToAddress("0x034edD2A225f7f429A63E0f1D2084B9E0A93b538"), + AbsolutePrestate: common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c"), + L2ChainID: big.NewInt(11155420), + Proposer: common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"), + }, + expectError: false, + }, + { + name: "missing proposer address", + input: BaseValidatorInput{ + SystemConfigAddress: common.HexToAddress("0x034edD2A225f7f429A63E0f1D2084B9E0A93b538"), + AbsolutePrestate: common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c"), + L2ChainID: big.NewInt(11155420), + Proposer: common.Address{}, + }, + expectError: true, + errorMsg: "proposer address is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var mockRPC *mockrpc.MockRPC + if !tt.expectError { + mockRPC = mockrpc.NewMockRPC( + t, + testlog.Logger(t, slog.LevelInfo), + mockrpc.WithOkCall("eth_chainId", mockrpc.NullMatcher(), "0xaa36a7"), // sepolia chain ID in hex + mockrpc.WithOkCall("eth_call", mockrpc.AnyParamsMatcher(), callResult), + ) + } else { + mockRPC = mockrpc.NewMockRPC(t, testlog.Logger(t, slog.LevelInfo)) + } + + rpcClient, err := rpc.Dial(mockRPC.Endpoint()) + require.NoError(t, err) + + validator := NewV500Validator(rpcClient) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + errCodes, err := validator.Validate(ctx, tt.input) + + if tt.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errorMsg) + } else { + require.NoError(t, err) + require.Equal(t, []string{"PDDG-40", "PDDG-DWETH-30", "PDDG-ANCHORP-40", "PLDG-40", "PLDG-DWETH-30", "PLDG-ANCHORP-40"}, errCodes) + mockRPC.AssertExpectations(t) + } + }) + } +}