Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validation: decouple SuperchainConfig tests #809

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 0 additions & 50 deletions validation/security-configs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,63 +9,13 @@ import (
. "github.com/ethereum-optimism/superchain-registry/superchain"
"github.com/ethereum-optimism/superchain-registry/validation/internal/bindings"
"github.com/ethereum-optimism/superchain-registry/validation/standard"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"
)

func getAddressFromConfig(chainID uint64, contractName string) (Address, error) {
if common.IsHexAddress(contractName) {
return Address(common.HexToAddress(contractName)), nil
}

contractAddress, err := Addresses[chainID].AddressFor(contractName)

return contractAddress, err
}

func getAddressFromChain(method string, contractAddress Address, client *ethclient.Client) (Address, error) {
addr := (common.Address(contractAddress))
callMsg := ethereum.CallMsg{
To: &addr,
Data: crypto.Keccak256([]byte(method))[:4],
}

// Make the call
callContract := func(msg ethereum.CallMsg) ([]byte, error) {
return client.CallContract(context.Background(), msg, nil)
}
result, err := Retry(callContract)(callMsg)
if err != nil {
return Address{}, err
}

return Address(common.BytesToAddress(result)), nil
}

var checkResolutions = func(t *testing.T, r standard.Resolutions, chainID uint64, client *ethclient.Client) {
for contract, methodToOutput := range r {
contractAddress, err := getAddressFromConfig(chainID, contract)
require.NoError(t, err)

for method, output := range methodToOutput {
want, err := getAddressFromConfig(chainID, output)
require.NoError(t, err)

got, err := getAddressFromChain(method, contractAddress, client)
require.NoErrorf(t, err, "problem calling %s.%s (%s)", contract, method, contractAddress)

// Use t.Errorf here for a concise output of failures, since failure info is sent to a slack channel
if want != got {
t.Errorf("%s.%s = %s, expected %s (%s)", contract, method, got, want, output)
}
}
}
}

func testL1SecurityConfig(t *testing.T, chain *ChainConfig) {
chainID := chain.ChainID

Expand Down
46 changes: 0 additions & 46 deletions validation/superchain-config_test.go

This file was deleted.

69 changes: 69 additions & 0 deletions validation/superchain_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package validation

import (
"fmt"
"testing"

. "github.com/ethereum-optimism/superchain-registry/superchain"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"
)

func testSuperchainConfig(t *testing.T, chain *ChainConfig) {
rpcEndpoint := Superchains[chain.Superchain].Config.L1.PublicRPC
require.NotEmpty(t, rpcEndpoint, "no rpc specified")

l1Client, err := ethclient.Dial(rpcEndpoint)
require.NoErrorf(t, err, "could not dial rpc endpoint %s", rpcEndpoint)
defer l1Client.Close()

err = checkSuperchainConfig(chain, l1Client)
require.NoError(t, err)
}

func checkSuperchainConfig(chain *ChainConfig, l1Client *ethclient.Client) error {
expected := Superchains[chain.Superchain].Config.SuperchainConfigAddr
if expected == nil {
return fmt.Errorf("Superchain does not declare a superchain_config_addr")
}

opcm := Superchains[chain.Superchain].Config.OPContractsManagerProxyAddr
if opcm == nil {
return fmt.Errorf("Superchain does not declare a op_contracts_manager_proxy_addr")
}

contracts := []Address{
Addresses[chain.ChainID].OptimismPortalProxy,
Addresses[chain.ChainID].AnchorStateRegistryProxy,
Addresses[chain.ChainID].L1CrossDomainMessengerProxy,
Addresses[chain.ChainID].L1ERC721BridgeProxy,
Addresses[chain.ChainID].L1StandardBridgeProxy,
Addresses[chain.ChainID].DelayedWETHProxy,
}
if err := verifySuperchainConfigOnchain(chain.ChainID, l1Client, contracts, *expected); err != nil {
return err
}
return nil
}

func verifySuperchainConfigOnchain(chainID uint64, l1Client EthClient, targetContracts []Address, expected Address) error {
for _, target := range targetContracts {
var got Address
var err error
if target == Addresses[chainID].DelayedWETHProxy {
// DelayedWETHProxy uses a different method name, so it is broken out here
got, err = getAddressFromChain("config()", target, l1Client)
} else {
got, err = getAddressFromChain("superchainConfig()", target, l1Client)
}

if err != nil {
return err
}

if expected != got {
return fmt.Errorf("incorrect superchainConfig() address: got %s, wanted %s (queried %s)", got, expected, target)
}
}
return nil
}
82 changes: 82 additions & 0 deletions validation/superchain_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package validation

import (
"errors"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"

"github.com/ethereum-optimism/superchain-registry/superchain"
"github.com/ethereum-optimism/superchain-registry/validation/testutils"
)

var (
testChainID = uint64(10)
delayedWETHProxyString = "0xabcdef0000000000000000000000000000000000"
normalContractString = "0x1234560000000000000000000000000000000000"
expectedAddressString = "0xaaaaaa0000000000000000000000000000000000"
)

// Setup some helper addresses and methods
func TestVerifyOnchain_Success(t *testing.T) {
t.Parallel()
delayedWETHProxy := superchain.MustHexToAddress(delayedWETHProxyString)
normalContract := superchain.MustHexToAddress(normalContractString)
expected := superchain.MustHexToAddress(expectedAddressString)
targetContracts := []superchain.Address{delayedWETHProxy, normalContract}

// For DelayedWETHProxy: method = "config()"
configMethodID := string(testutils.MethodID("config()"))
configResponse := common.HexToAddress(expectedAddressString).Bytes()

// For other contracts: method = "superchainConfig()"
superchainConfigMethodID := string(testutils.MethodID("superchainConfig()"))
superchainConfigResponse := common.HexToAddress(expectedAddressString).Bytes()

mockClient := &testutils.MockEthClient{
Responses: map[string][]byte{
configMethodID: configResponse,
superchainConfigMethodID: superchainConfigResponse,
},
}

err := verifySuperchainConfigOnchain(testChainID, mockClient, targetContracts, expected)
require.NoError(t, err)
}

func TestVerifyOnchain_ErrorFromCall(t *testing.T) {
t.Parallel()
delayedWETHProxy := superchain.MustHexToAddress(delayedWETHProxyString)
expected := superchain.MustHexToAddress("0xabcdef0000000000000000000000000000000000")
targetContracts := []superchain.Address{delayedWETHProxy}

mockClient := &testutils.MockEthClient{
Err: errors.New("test call contract error"),
}

err := verifySuperchainConfigOnchain(testChainID, mockClient, targetContracts, expected)
require.Error(t, err)
require.Contains(t, err.Error(), "test call contract error")
}

func TestVerifyOnchain_IncorrectAddress(t *testing.T) {
t.Parallel()
delayedWETHProxy := superchain.MustHexToAddress(delayedWETHProxyString)
normalContract := superchain.MustHexToAddress(normalContractString)
expected := superchain.MustHexToAddress(expectedAddressString)
targetContracts := []superchain.Address{delayedWETHProxy, normalContract}

// Response returns a different address than expected
mockClient := &testutils.MockEthClient{
Responses: map[string][]byte{
string(testutils.MethodID("superchainConfig()")): common.HexToAddress("0x9999990000000000000000000000000000000000").Bytes(),
},
}

err := verifySuperchainConfigOnchain(testChainID, mockClient, targetContracts, expected)
require.Error(t, err)
require.Contains(t, err.Error(), "incorrect superchainConfig() address")
require.Contains(t, err.Error(), "wanted 0xAAaAaA0000000000000000000000000000000000")
require.Contains(t, err.Error(), "got 0x9999990000000000000000000000000000000000")
}
6 changes: 2 additions & 4 deletions validation/testutils/mock_ethclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ import (
"github.com/ethereum/go-ethereum/crypto"
)

// MockEthClient is a mock that implements EthCaller for testing.
type MockEthClient struct {
// Setup response maps keyed by method 4-byte signature + address, or
// just by method signature if that's simpler for your tests.
// Response map keyed by method 4-byte signature
Responses map[string][]byte
Err error
}
Expand All @@ -23,7 +21,7 @@ func (m *MockEthClient) CallContract(ctx context.Context, msg ethereum.CallMsg,
return m.Responses[string(msg.Data)], nil
}

// Helper to get the 4-byte method ID (like what getBytes does internally)
// Helper to get the 4-byte method ID
func MethodID(method string) []byte {
return crypto.Keccak256([]byte(method))[:4]
}
54 changes: 54 additions & 0 deletions validation/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,68 @@ import (

"github.com/ethereum-optimism/optimism/op-service/retry"
"github.com/ethereum-optimism/superchain-registry/superchain"
"github.com/ethereum-optimism/superchain-registry/validation/standard"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type EthClient interface {
CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error)
}

func getAddressFromConfig(chainID uint64, contractName string) (superchain.Address, error) {
if common.IsHexAddress(contractName) {
return superchain.Address(common.HexToAddress(contractName)), nil
}

contractAddress, err := superchain.Addresses[chainID].AddressFor(contractName)

return contractAddress, err
}

func getAddressFromChain(method string, contractAddress superchain.Address, client EthClient) (superchain.Address, error) {
addr := (common.Address(contractAddress))
callMsg := ethereum.CallMsg{
To: &addr,
Data: crypto.Keccak256([]byte(method))[:4],
}

// Make the call
callContract := func(msg ethereum.CallMsg) ([]byte, error) {
return client.CallContract(context.Background(), msg, nil)
}
result, err := Retry(callContract)(callMsg)
if err != nil {
return superchain.Address{}, err
}

return superchain.Address(common.BytesToAddress(result)), nil
}

var checkResolutions = func(t *testing.T, r standard.Resolutions, chainID uint64, client *ethclient.Client) {
for contract, methodToOutput := range r {
contractAddress, err := getAddressFromConfig(chainID, contract)
require.NoError(t, err)

for method, output := range methodToOutput {
want, err := getAddressFromConfig(chainID, output)
require.NoError(t, err)

got, err := getAddressFromChain(method, contractAddress, client)
require.NoErrorf(t, err, "problem calling %s.%s (%s)", contract, method, contractAddress)

// Use t.Errorf here for a concise output of failures, since failure info is sent to a slack channel
if want != got {
t.Errorf("%s.%s = %s, expected %s (%s)", contract, method, got, want, output)
}
}
}
}

// isBigIntWithinBounds returns true if actual is within bounds, where the bounds are [lower bound, upper bound] and are inclusive.
var isBigIntWithinBounds = func(actual *big.Int, bounds [2]*big.Int) bool {
if (bounds[1].Cmp(bounds[0])) < 0 {
Expand Down