From 59f7927e86e5c7308687517cd961d2af43b80dd7 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 3 Apr 2024 18:28:54 +0000 Subject: [PATCH] refactor(delegation): validate + load genesis --- x/delegation/keeper/cross_chain_tx_process.go | 35 ++- x/delegation/keeper/genesis.go | 35 ++- x/delegation/types/genesis.go | 49 ++-- x/delegation/types/genesis_test.go | 236 ++++++++++++++++++ 4 files changed, 323 insertions(+), 32 deletions(-) create mode 100644 x/delegation/types/genesis_test.go diff --git a/x/delegation/keeper/cross_chain_tx_process.go b/x/delegation/keeper/cross_chain_tx_process.go index a88ddaa9d..ca2a39161 100644 --- a/x/delegation/keeper/cross_chain_tx_process.go +++ b/x/delegation/keeper/cross_chain_tx_process.go @@ -95,15 +95,28 @@ import ( }, nil }*/ -// DelegateTo : It doesn't need to check the active status of the operator in middlewares when delegating assets to the operator. This is because it adds assets to the operator's amount. But it needs to check if operator has been slashed or frozen. -func (k *Keeper) DelegateTo(ctx sdk.Context, params *delegationtype.DelegationOrUndelegationParams) error { +// DelegateTo : It doesn't need to check the active status of the operator in middlewares when +// delegating assets to the operator. This is because it adds assets to the operator's amount. +// But it needs to check if operator has been slashed or frozen. +func (k Keeper) DelegateTo(ctx sdk.Context, params *delegationtype.DelegationOrUndelegationParams) error { + return k.delegateTo(ctx, params, true) +} + +// delegateTo is the internal private version of DelegateTo. if the notGenesis parameter is +// false, the operator keeper and the delegation hooks are not called. +func (k *Keeper) delegateTo( + ctx sdk.Context, + params *delegationtype.DelegationOrUndelegationParams, + notGenesis bool, +) error { // check if the delegatedTo address is an operator if !k.operatorKeeper.IsOperator(ctx, params.OperatorAddress) { return errorsmod.Wrap(delegationtype.ErrOperatorNotExist, fmt.Sprintf("input operatorAddr is:%s", params.OperatorAddress)) } // check if the operator has been slashed or frozen - if k.slashKeeper.IsOperatorFrozen(ctx, params.OperatorAddress) { + // skip the check if not genesis (or chain restart) + if notGenesis && k.slashKeeper.IsOperatorFrozen(ctx, params.OperatorAddress) { return delegationtype.ErrOperatorIsFrozen } @@ -153,14 +166,16 @@ func (k *Keeper) DelegateTo(ctx sdk.Context, params *delegationtype.DelegationOr if err != nil { return err } - // call operator module to bond the increased assets to the opted-in AVS - err = k.operatorKeeper.UpdateOptedInAssetsState(ctx, stakerID, assetID, params.OperatorAddress.String(), params.OpAmount) - if err != nil { - return err - } - // call the hooks registered by the other modules - k.Hooks().AfterDelegation(ctx, params.OperatorAddress) + if notGenesis { + // call operator module to bond the increased assets to the opted-in AVS + err = k.operatorKeeper.UpdateOptedInAssetsState(ctx, stakerID, assetID, params.OperatorAddress.String(), params.OpAmount) + if err != nil { + return err + } + // call the hooks registered by the other modules + k.Hooks().AfterDelegation(ctx, params.OperatorAddress) + } return nil } diff --git a/x/delegation/keeper/genesis.go b/x/delegation/keeper/genesis.go index d4ae8059e..82612967b 100644 --- a/x/delegation/keeper/genesis.go +++ b/x/delegation/keeper/genesis.go @@ -1,18 +1,49 @@ package keeper import ( + assetstype "github.com/ExocoreNetwork/exocore/x/assets/types" delegationtype "github.com/ExocoreNetwork/exocore/x/delegation/types" abci "github.com/cometbft/cometbft/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" ) // InitGenesis initializes the module's state from a provided genesis state. // Since this action typically occurs on chain starts, this function is allowed to panic. func (k Keeper) InitGenesis( ctx sdk.Context, - genState delegationtype.GenesisState, + gs delegationtype.GenesisState, ) []abci.ValidatorUpdate { - // TODO + for _, level1 := range gs.Delegations { + stakerID := level1.StakerID + // #nosec G703 // already validated + stakerAddress, lzID, _ := assetstype.ParseID(stakerID) + // we have checked IsHexAddress already + stakerAddressBytes := common.HexToAddress(stakerAddress) + for _, level2 := range level1.Delegations { + assetID := level2.AssetID + // #nosec G703 // already validated + assetAddress, _, _ := assetstype.ParseID(assetID) + // we have checked IsHexAddress already + assetAddressBytes := common.HexToAddress(assetAddress) + for operator, wrappedAmount := range level2.PerOperatorAmounts { + amount := wrappedAmount.Amount + // #nosec G703 // already validated + accAddress, _ := sdk.AccAddressFromBech32(operator) + delegationParams := &delegationtype.DelegationOrUndelegationParams{ + ClientChainLzID: lzID, + Action: assetstype.DelegateTo, + AssetsAddress: assetAddressBytes.Bytes(), + OperatorAddress: accAddress, + StakerAddress: stakerAddressBytes.Bytes(), + OpAmount: amount, + } + if err := k.delegateTo(ctx, delegationParams, false); err != nil { + panic(err) + } + } + } + } return []abci.ValidatorUpdate{} } diff --git a/x/delegation/types/genesis.go b/x/delegation/types/genesis.go index 4f83b97b7..bae0fae7d 100644 --- a/x/delegation/types/genesis.go +++ b/x/delegation/types/genesis.go @@ -27,8 +27,12 @@ func (gs GenesisState) Validate() error { for _, level1 := range gs.Delegations { stakerID := level1.StakerID // validate staker ID - if _, _, err := assetstypes.ValidateID(stakerID, true); err != nil { - return errorsmod.Wrapf(err, "invalid staker ID %s", stakerID) + var stakerClientChainID uint64 + var err error + if _, stakerClientChainID, err = assetstypes.ValidateID(stakerID, true); err != nil { + return errorsmod.Wrapf( + ErrInvalidGenesisData, "invalid staker ID %s: %s", stakerID, err, + ) } // check for duplicate stakers if _, ok := stakers[stakerID]; ok { @@ -38,23 +42,35 @@ func (gs GenesisState) Validate() error { assets := make(map[string]struct{}, len(level1.Delegations)) for _, level2 := range level1.Delegations { assetID := level2.AssetID - // validate asset ID - if _, _, err := assetstypes.ValidateID(assetID, true); err != nil { - return errorsmod.Wrapf(err, "invalid asset ID %s", assetID) - } // check for duplicate assets if _, ok := assets[assetID]; ok { return errorsmod.Wrapf(ErrInvalidGenesisData, "duplicate asset ID %s", assetID) } assets[assetID] = struct{}{} + // validate asset ID + var assetClientChainID uint64 + if _, assetClientChainID, err = assetstypes.ValidateID(assetID, true); err != nil { + return errorsmod.Wrapf( + ErrInvalidGenesisData, "invalid asset ID %s: %s", assetID, err, + ) + } + if assetClientChainID != stakerClientChainID { + // a staker from chain A is delegating an asset on chain B, which is not + // something we support right now. + return errorsmod.Wrapf( + ErrInvalidGenesisData, + "asset %s client chain ID %d does not match staker %s client chain ID %d", + assetID, assetClientChainID, stakerID, stakerClientChainID, + ) + } givenTotal := level2.TotalDelegatedAmount - if givenTotal.IsNegative() || givenTotal.IsNil() { + // in this if condition, check nil first to avoid panic + if givenTotal.IsNil() || givenTotal.IsNegative() { return errorsmod.Wrapf( - ErrInvalidGenesisData, "invalid total delegated amount %d", givenTotal, + ErrInvalidGenesisData, "invalid total delegated amount %s", givenTotal, ) } calculatedTotal := sdk.ZeroInt() - operators := make(map[string]struct{}, len(level2.PerOperatorAmounts)) for operator, wrappedAmount := range level2.PerOperatorAmounts { // check supplied amount if wrappedAmount == nil { @@ -63,10 +79,10 @@ func (gs GenesisState) Validate() error { ) } amount := wrappedAmount.Amount - if amount.IsNegative() || amount.IsNil() { + if amount.IsNil() || amount.IsNegative() { return errorsmod.Wrapf( ErrInvalidGenesisData, - "invalid operator amount %d for operator %s", amount, operator, + "invalid operator amount %s for operator %s", amount, operator, ) } // check operator address @@ -76,20 +92,13 @@ func (gs GenesisState) Validate() error { "invalid operator address for operator %s", operator, ) } - // check for duplicate operators - if _, ok := operators[operator]; ok { - return errorsmod.Wrapf( - ErrInvalidGenesisData, - "duplicate operator %s for asset %s", operator, assetID, - ) - } - operators[operator] = struct{}{} + // no need to check for duplicate operators, since it is already a map. calculatedTotal = calculatedTotal.Add(amount) } if !givenTotal.Equal(calculatedTotal) { return errorsmod.Wrapf( ErrInvalidGenesisData, - "total delegated amount %d does not match calculated total %d for asset %s", + "total delegated amount %s does not match calculated total %s for asset %s", givenTotal, calculatedTotal, assetID, ) } diff --git a/x/delegation/types/genesis_test.go b/x/delegation/types/genesis_test.go new file mode 100644 index 000000000..13d39c403 --- /dev/null +++ b/x/delegation/types/genesis_test.go @@ -0,0 +1,236 @@ +package types_test + +import ( + "testing" + + "cosmossdk.io/math" + utiltx "github.com/ExocoreNetwork/exocore/testutil/tx" + assetstypes "github.com/ExocoreNetwork/exocore/x/assets/types" + "github.com/ExocoreNetwork/exocore/x/delegation/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "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() { + assetAddress := utiltx.GenerateAddress() + stakerAddress := utiltx.GenerateAddress() + lzID := uint64(101) + stakerID, assetID := assetstypes.GetStakeIDAndAssetID( + lzID, stakerAddress[:], assetAddress[:], + ) + operatorAddress := sdk.AccAddress(utiltx.GenerateAddress().Bytes()) + delegations := []types.DelegationsByStaker{ + { + StakerID: stakerID, + Delegations: []types.DelegatedSingleAssetInfo{ + { + AssetID: assetID, + TotalDelegatedAmount: math.NewInt(1000), + PerOperatorAmounts: map[string]*types.ValueField{ + operatorAddress.String(): {Amount: math.NewInt(1000)}, + }, + }, + }, + }, + } + testCases := []struct { + name string + genState *types.GenesisState + expPass bool + malleate func(*types.GenesisState) + unmalleate func(*types.GenesisState) + }{ + { + name: "valid empty genesis", + genState: &types.GenesisState{}, + expPass: true, + }, + { + name: "default", + genState: types.DefaultGenesis(), + expPass: true, + }, + { + name: "base, should pass", + genState: types.NewGenesis(delegations), + expPass: true, + }, + { + name: "invalid staker id", + genState: types.NewGenesis(delegations), + expPass: false, + malleate: func(gs *types.GenesisState) { + gs.Delegations[0].StakerID = "invalid" + }, + unmalleate: func(gs *types.GenesisState) { + gs.Delegations[0].StakerID = stakerID + }, + }, + { + name: "duplicate staker id", + genState: types.NewGenesis(delegations), + expPass: false, + malleate: func(gs *types.GenesisState) { + gs.Delegations = append(gs.Delegations, gs.Delegations[0]) + }, + unmalleate: func(gs *types.GenesisState) { + gs.Delegations = gs.Delegations[:1] + }, + }, + { + name: "duplicate asset id", + genState: types.NewGenesis(delegations), + expPass: false, + malleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations = append( + gs.Delegations[0].Delegations, + gs.Delegations[0].Delegations[0], + ) + }, + unmalleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations = gs.Delegations[0].Delegations[:1] + }, + }, + { + name: "invalid asset id", + genState: types.NewGenesis(delegations), + expPass: false, + malleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations[0].AssetID = "invalid" + }, + unmalleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations[0].AssetID = assetID + }, + }, + { + name: "asset id mismatch", + genState: types.NewGenesis(delegations), + expPass: false, + malleate: func(gs *types.GenesisState) { + stakerID, _ := assetstypes.GetStakeIDAndAssetID( + lzID+1, stakerAddress[:], assetAddress[:], + ) + gs.Delegations[0].StakerID = stakerID + }, + unmalleate: func(gs *types.GenesisState) { + gs.Delegations[0].StakerID = stakerID + }, + }, + { + name: "invalid total amount", + genState: types.NewGenesis(delegations), + expPass: false, + malleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations[0].TotalDelegatedAmount = math.NewInt(-1) + }, + unmalleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations[0].TotalDelegatedAmount = math.NewInt(1000) + }, + }, + { + name: "nil total amount", + genState: types.NewGenesis(delegations), + expPass: false, + malleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations[0].TotalDelegatedAmount = math.Int{} + }, + unmalleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations[0].TotalDelegatedAmount = math.NewInt(1000) + }, + }, + { + name: "nil wrapped amount", + genState: types.NewGenesis(delegations), + expPass: false, + malleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations[0].PerOperatorAmounts[operatorAddress.String()] = + nil + }, + unmalleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations[0].PerOperatorAmounts[operatorAddress.String()] = + &types.ValueField{Amount: math.NewInt(1000)} + }, + }, + { + name: "nil unwrapped amount", + genState: types.NewGenesis(delegations), + expPass: false, + malleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations[0].PerOperatorAmounts[operatorAddress.String()] = + &types.ValueField{} + }, + unmalleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations[0].PerOperatorAmounts[operatorAddress.String()] = + &types.ValueField{Amount: math.NewInt(1000)} + }, + }, + { + name: "negative unwrapped amount", + genState: types.NewGenesis(delegations), + expPass: false, + malleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations[0].PerOperatorAmounts[operatorAddress.String()] = + &types.ValueField{Amount: math.NewInt(-1)} + }, + unmalleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations[0].PerOperatorAmounts[operatorAddress.String()] = + &types.ValueField{Amount: math.NewInt(1000)} + }, + }, + { + name: "invalid operator address", + genState: types.NewGenesis(delegations), + expPass: false, + malleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations[0].PerOperatorAmounts["invalid"] = + &types.ValueField{Amount: math.NewInt(1000)} + }, + unmalleate: func(gs *types.GenesisState) { + delete(gs.Delegations[0].Delegations[0].PerOperatorAmounts, "invalid") + }, + }, + { + name: "total amount mismatch", + genState: types.NewGenesis(delegations), + expPass: false, + malleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations[0].PerOperatorAmounts[operatorAddress.String()] = + &types.ValueField{Amount: math.NewInt(2000)} + }, + unmalleate: func(gs *types.GenesisState) { + gs.Delegations[0].Delegations[0].PerOperatorAmounts[operatorAddress.String()] = + &types.ValueField{Amount: math.NewInt(1000)} + }, + }, + } + + for _, tc := range testCases { + tc := tc + if tc.malleate != nil { + tc.malleate(tc.genState) + // require that unmalleate is defined + suite.Require().NotNil(tc.unmalleate, tc.name) + } + err := tc.genState.Validate() + if tc.expPass { + suite.Require().NoError(err, tc.name) + } else { + suite.Require().Error(err, tc.name) + } + if tc.unmalleate != nil { + tc.unmalleate(tc.genState) + } + // fmt.Println(tc.name, err) + } +}