diff --git a/x/operator/types/genesis.go b/x/operator/types/genesis.go index c8eb1a742..da35d9e05 100644 --- a/x/operator/types/genesis.go +++ b/x/operator/types/genesis.go @@ -38,6 +38,9 @@ func (gs GenesisState) Validate() error { } // consensus_keys.go operatorsByKeys := make(map[string]struct{}, len(gs.OperatorRecords)) + // keysByChainId stores chain id -> cons keys list. ensure that within a chain id, cons key + // isn't repeated. + keysByChainId := make(map[string](map[string]struct{})) for _, record := range gs.OperatorRecords { operatorAddress := record.OperatorAddress _, err := sdk.AccAddressFromBech32(operatorAddress) @@ -72,8 +75,23 @@ func (gs GenesisState) Validate() error { consKeyString, err, ) } + // validate chain id is not done, since it is not strictly enforced within Cosmos. + if _, ok := keysByChainId[subRecord.ChainID]; !ok { + keysByChainId[subRecord.ChainID] = make(map[string]struct{}) + } + if _, ok := keysByChainId[subRecord.ChainID][consKeyString]; ok { + return errorsmod.Wrapf( + ErrInvalidGenesisData, + "duplicate consensus key %s for chain %s", + consKeyString, subRecord.ChainID, + ) + } + keysByChainId[subRecord.ChainID][consKeyString] = struct{}{} } } + // it may be possible for an operator to opt into an AVS which does not have a consensus + // key requirement, so this check could be removed if we set up the Export case. but i + // think it is better to keep it for now. if len(operators) != len(operatorsByKeys) { return errorsmod.Wrapf( ErrInvalidGenesisData, @@ -150,12 +168,10 @@ func (gs GenesisState) Validate() error { } } } - if len(operators) != len(operatorsByStakers) { - return errorsmod.Wrapf( - ErrInvalidGenesisData, - "operator addresses in operators and staker records do not match", - ) - } + // it is possible that a few operators do not get delegations from stakers. that means + // operatorsByStakers may be smaller than operators. + // operatorsByStakers can never be larger than operators anyway, since we have checked + // that operators are already registered. // it may also be prudent to validate the sorted (or not) nature of these items // but it is not critical for the functioning. it is only used for comparison // of the genesis state stored across all of the validators. diff --git a/x/operator/types/genesis_test.go b/x/operator/types/genesis_test.go new file mode 100644 index 000000000..67ecc626a --- /dev/null +++ b/x/operator/types/genesis_test.go @@ -0,0 +1,546 @@ +package types_test + +import ( + "testing" + + "cosmossdk.io/math" + utiltx "github.com/ExocoreNetwork/exocore/testutil/tx" + "github.com/ExocoreNetwork/exocore/utils" + "github.com/ExocoreNetwork/exocore/x/operator/types" + "github.com/cometbft/cometbft/crypto/ed25519" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/suite" +) + +type GenesisTestSuite struct { + suite.Suite +} + +func (suite *GenesisTestSuite) SetupTest() { +} + +func TestGenesisTestSuite(t *testing.T) { + suite.Run(t, new(GenesisTestSuite)) +} + +func (suite *GenesisTestSuite) TestValidateGenesis() { + key := hexutil.Encode(ed25519.GenPrivKey().PubKey().Bytes()) + accAddress1 := sdk.AccAddress(utiltx.GenerateAddress().Bytes()) + accAddress2 := sdk.AccAddress(utiltx.GenerateAddress().Bytes()) + newGen := &types.GenesisState{} + + testCases := []struct { + name string + genState *types.GenesisState + expPass bool + malleate func(*types.GenesisState) + }{ + { + name: "valid genesis constructor", + genState: newGen, + expPass: true, + }, + { + name: "default", + genState: types.DefaultGenesis(), + expPass: true, + }, + { + name: "invalid genesis state due to non bech32 operator address", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: "invalid", + }, + }, + }, + expPass: false, + }, + { + name: "invalid genesis state due to duplicate operator address", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: accAddress1.String(), + }, + { + EarningsAddr: accAddress1.String(), + }, + }, + }, + expPass: false, + }, + { + name: "invalid genesis state due to invalid cons key operator address", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: accAddress1.String(), + }, + }, + OperatorRecords: []types.OperatorConsKeyRecord{ + { + OperatorAddress: "invalid", + }, + }, + }, + expPass: false, + }, + { + name: "invalid genesis state due to unregistered operator in cons key", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: accAddress1.String(), + }, + }, + OperatorRecords: []types.OperatorConsKeyRecord{ + { + OperatorAddress: accAddress2.String(), + }, + }, + }, + expPass: false, + }, + { + name: "invalid genesis state due to duplicate operator in cons key", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: accAddress1.String(), + }, + { + EarningsAddr: accAddress2.String(), + }, + }, + OperatorRecords: []types.OperatorConsKeyRecord{ + { + OperatorAddress: accAddress1.String(), + Chains: []types.ChainDetails{ + { + ChainID: utils.TestnetChainID, + ConsensusKey: key, + }, + }, + }, + { + OperatorAddress: accAddress1.String(), + Chains: []types.ChainDetails{ + { + ChainID: utils.TestnetChainID, + ConsensusKey: hexutil.Encode(ed25519.GenPrivKey().PubKey().Bytes()), + }, + }, + }, + }, + }, + expPass: false, + }, + { + name: "invalid genesis state due to invalid cons key", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: accAddress1.String(), + }, + }, + OperatorRecords: []types.OperatorConsKeyRecord{ + { + OperatorAddress: accAddress1.String(), + Chains: []types.ChainDetails{ + { + ChainID: utils.TestnetChainID, + ConsensusKey: key + "fake", + }, + }, + }, + }, + }, + expPass: false, + }, + { + name: "invalid genesis state due to duplicate cons key for the same chain id", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: accAddress1.String(), + }, + { + EarningsAddr: accAddress2.String(), + }, + }, + OperatorRecords: []types.OperatorConsKeyRecord{ + { + OperatorAddress: accAddress1.String(), + Chains: []types.ChainDetails{ + { + ChainID: utils.TestnetChainID, + ConsensusKey: key, + }, + }, + }, + { + OperatorAddress: accAddress2.String(), + Chains: []types.ChainDetails{ + { + ChainID: utils.TestnetChainID, + ConsensusKey: key, + }, + }, + }, + }, + }, + expPass: false, + }, + { + name: "invalid genesis state due to registered operator without key", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: accAddress1.String(), + }, + { + EarningsAddr: accAddress2.String(), + }, + }, + OperatorRecords: []types.OperatorConsKeyRecord{ + { + OperatorAddress: accAddress1.String(), + Chains: []types.ChainDetails{ + { + ChainID: utils.TestnetChainID, + ConsensusKey: key, + }, + }, + }, + }, + }, + expPass: false, + }, + { + name: "invalid genesis state due to invalid staker id", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: accAddress1.String(), + }, + }, + OperatorRecords: []types.OperatorConsKeyRecord{ + { + OperatorAddress: accAddress1.String(), + Chains: []types.ChainDetails{ + { + ChainID: utils.TestnetChainID, + ConsensusKey: key, + }, + }, + }, + }, + StakerRecords: []types.StakerRecord{ + { + StakerID: "invalid", + }, + }, + }, + expPass: false, + }, + { + name: "invalid genesis state due to duplicate staker id", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: accAddress1.String(), + }, + }, + OperatorRecords: []types.OperatorConsKeyRecord{ + { + OperatorAddress: accAddress1.String(), + Chains: []types.ChainDetails{ + { + ChainID: utils.TestnetChainID, + ConsensusKey: key, + }, + }, + }, + }, + StakerRecords: []types.StakerRecord{ + { + StakerID: "asd_0x64", + }, + { + StakerID: "asd_0x64", + }, + }, + }, + expPass: false, + }, + { + name: "invalid genesis state due to invalid asset id", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: accAddress1.String(), + }, + }, + OperatorRecords: []types.OperatorConsKeyRecord{ + { + OperatorAddress: accAddress1.String(), + Chains: []types.ChainDetails{ + { + ChainID: utils.TestnetChainID, + ConsensusKey: key, + }, + }, + }, + }, + StakerRecords: []types.StakerRecord{ + { + StakerID: "asd_0x64", + StakerDetails: []types.StakerDetails{ + { + AssetID: "", + }, + }, + }, + }, + }, + expPass: false, + }, + { + name: "invalid genesis state due to duplicate asset id", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: accAddress1.String(), + }, + }, + OperatorRecords: []types.OperatorConsKeyRecord{ + { + OperatorAddress: accAddress1.String(), + Chains: []types.ChainDetails{ + { + ChainID: utils.TestnetChainID, + ConsensusKey: key, + }, + }, + }, + }, + StakerRecords: []types.StakerRecord{ + { + StakerID: "asd_0x64", + StakerDetails: []types.StakerDetails{ + { + AssetID: "dead_0x64", + }, + { + AssetID: "dead_0x64", + }, + }, + }, + }, + }, + expPass: false, + }, + { + name: "invalid genesis state due to invalid operator address", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: accAddress1.String(), + }, + }, + OperatorRecords: []types.OperatorConsKeyRecord{ + { + OperatorAddress: accAddress1.String(), + Chains: []types.ChainDetails{ + { + ChainID: utils.TestnetChainID, + ConsensusKey: key, + }, + }, + }, + }, + StakerRecords: []types.StakerRecord{ + { + StakerID: "asd_0x64", + StakerDetails: []types.StakerDetails{ + { + AssetID: "dead_0x64", + Details: []types.AssetDetails{ + { + OperatorAddress: "fake", + }, + }, + }, + }, + }, + }, + }, + expPass: false, + }, + { + name: "invalid genesis state due to unregsitered operator address", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: accAddress1.String(), + }, + }, + OperatorRecords: []types.OperatorConsKeyRecord{ + { + OperatorAddress: accAddress1.String(), + Chains: []types.ChainDetails{ + { + ChainID: utils.TestnetChainID, + ConsensusKey: key, + }, + }, + }, + }, + StakerRecords: []types.StakerRecord{ + { + StakerID: "asd_0x64", + StakerDetails: []types.StakerDetails{ + { + AssetID: "dead_0x64", + Details: []types.AssetDetails{ + { + OperatorAddress: accAddress2.String(), + }, + }, + }, + }, + }, + }, + }, + expPass: false, + }, + { + name: "invalid genesis state due to negative amount", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: accAddress1.String(), + }, + }, + OperatorRecords: []types.OperatorConsKeyRecord{ + { + OperatorAddress: accAddress1.String(), + Chains: []types.ChainDetails{ + { + ChainID: utils.TestnetChainID, + ConsensusKey: key, + }, + }, + }, + }, + StakerRecords: []types.StakerRecord{ + { + StakerID: "asd_0x64", + StakerDetails: []types.StakerDetails{ + { + AssetID: "dead_0x64", + Details: []types.AssetDetails{ + { + OperatorAddress: accAddress1.String(), + Amount: math.NewInt(-1), + }, + }, + }, + }, + }, + }, + }, + expPass: false, + }, + { + name: "invalid genesis state due to nil amount", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: accAddress1.String(), + }, + }, + OperatorRecords: []types.OperatorConsKeyRecord{ + { + OperatorAddress: accAddress1.String(), + Chains: []types.ChainDetails{ + { + ChainID: utils.TestnetChainID, + ConsensusKey: key, + }, + }, + }, + }, + StakerRecords: []types.StakerRecord{ + { + StakerID: "asd_0x64", + StakerDetails: []types.StakerDetails{ + { + AssetID: "dead_0x64", + Details: []types.AssetDetails{ + { + OperatorAddress: accAddress1.String(), + }, + }, + }, + }, + }, + }, + }, + expPass: false, + }, + { + name: "valid genesis state", + genState: &types.GenesisState{ + Operators: []types.OperatorInfo{ + { + EarningsAddr: accAddress1.String(), + }, + }, + OperatorRecords: []types.OperatorConsKeyRecord{ + { + OperatorAddress: accAddress1.String(), + Chains: []types.ChainDetails{ + { + ChainID: utils.TestnetChainID, + ConsensusKey: key, + }, + }, + }, + }, + StakerRecords: []types.StakerRecord{ + { + StakerID: "asd_0x64", + StakerDetails: []types.StakerDetails{ + { + AssetID: "dead_0x64", + Details: []types.AssetDetails{ + { + OperatorAddress: accAddress1.String(), + Amount: math.NewInt(1), + }, + }, + }, + }, + }, + }, + }, + expPass: true, + }, + } + + for _, tc := range testCases { + tc := tc + if tc.malleate != nil { + tc.malleate(tc.genState) + } + err := tc.genState.Validate() + if tc.expPass { + suite.Require().NoError(err, tc.name) + } else { + suite.Require().Error(err, tc.name) + } + } +}