diff --git a/INTEGRATION.md b/INTEGRATION.md index 14eea61..91b3068 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -56,6 +56,7 @@ app.POAKeeper = poakeeper.NewKeeper( runtime.NewKVStoreService(keys[poatypes.StoreKey]), app.StakingKeeper, app.SlashingKeeper, + app.BankKeeper, authcodec.NewBech32Codec(sdk.Bech32PrefixValAddr), logger, ) @@ -75,7 +76,6 @@ app.ModuleManager = module.NewManager( app.ModuleManager.SetOrderBeginBlockers( ... poa.ModuleName, - stakingtypes.ModuleName, ... ) @@ -83,7 +83,6 @@ app.ModuleManager.SetOrderBeginBlockers( app.ModuleManager.SetOrderEndBlockers( ... poa.ModuleName, - stakingtypes.ModuleName, ... ) @@ -91,7 +90,6 @@ app.ModuleManager.SetOrderEndBlockers( // NOTE: This must be after the staking module init genesis app.ModuleManager.SetOrderInitGenesis( ... - stakingtypes.ModuleName, poa.ModuleName, ... ) @@ -164,7 +162,7 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { ### Slashing - Genesis Params -When setting up your network genesis, it is important to consider setting slash infractions to 0%. +When setting up your network genesis, it is important to consider setting slash infractions to 0%. Setting downtime is more reasonable to diminish their weight in the network. ```json app_state.slashing.params.slash_fraction_double_sign diff --git a/keeper/expected_keepers.go b/keeper/expected_keepers.go new file mode 100644 index 0000000..944ab08 --- /dev/null +++ b/keeper/expected_keepers.go @@ -0,0 +1,54 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" +) + +type BankKeeper interface { + GetBalance(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin + MintCoins(ctx context.Context, moduleName string, amt sdk.Coins) error + SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error + SendCoinsFromModuleToModule(ctx context.Context, senderModule, recipientModule string, amt sdk.Coins) error +} + +type SlashingKeeper interface { + DeleteMissedBlockBitmap(ctx context.Context, addr sdk.ConsAddress) error + SetValidatorSigningInfo(ctx context.Context, address sdk.ConsAddress, info slashingtypes.ValidatorSigningInfo) error +} + +// We use almost all the methods from StakingKeeper. Just using it directly +/* +type StakingKeeper interface { + Hooks() stakingtypes.StakingHooks + + SetParams(ctx context.Context, params stakingtypes.Params) error + SetLastValidatorPower(ctx context.Context, operator sdk.ValAddress, power int64) error + SetDelegation(ctx context.Context, delegation stakingtypes.Delegation) error + SetValidator(ctx context.Context, validator stakingtypes.Validator) error + SetValidatorByConsAddr(ctx context.Context, validator stakingtypes.Validator) error + SetNewValidatorByPowerIndex(ctx context.Context, validator stakingtypes.Validator) error + SetLastTotalPower(ctx context.Context, power math.Int) error + DeleteLastValidatorPower(ctx context.Context, operator sdk.ValAddress) error + GetLastTotalPower(ctx context.Context) (math.Int, error) + + Slash(ctx context.Context, consAddr sdk.ConsAddress, infractionHeight, power int64, slashFactor math.LegacyDec) (math.Int, error) + + MinCommissionRate(ctx context.Context) (math.LegacyDec, error) + BondDenom(ctx context.Context) (string, error) + + GetValidator(ctx context.Context, addr sdk.ValAddress) (validator stakingtypes.Validator, err error) + GetAllValidators(ctx context.Context) (validators []stakingtypes.Validator, err error) + GetValidatorByConsAddr(ctx context.Context, consAddr sdk.ConsAddress) (validator stakingtypes.Validator, err error) + GetLastValidatorPower(ctx context.Context, operator sdk.ValAddress) (power int64, err error) + + GetAllDelegations(ctx context.Context) (delegations []stakingtypes.Delegation, err error) + + TokensFromConsensusPower(ctx context.Context, power int64) math.Int + TokensToConsensusPower(ctx context.Context, tokens math.Int) int64 + + PowerReduction(ctx context.Context) math.Int +} +*/ diff --git a/keeper/keeper.go b/keeper/keeper.go index 4f243c7..2b8748e 100644 --- a/keeper/keeper.go +++ b/keeper/keeper.go @@ -6,13 +6,16 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "cosmossdk.io/collections" addresscodec "cosmossdk.io/core/address" storetypes "cosmossdk.io/core/store" "cosmossdk.io/log" + sdkmath "cosmossdk.io/math" "github.com/strangelove-ventures/poa" ) @@ -22,7 +25,8 @@ type Keeper struct { validatorAddressCodec addresscodec.Codec stakingKeeper *stakingkeeper.Keeper - slashKeeper slashingkeeper.Keeper + slashKeeper SlashingKeeper + bankKeeper BankKeeper logger log.Logger @@ -40,7 +44,8 @@ func NewKeeper( cdc codec.BinaryCodec, storeService storetypes.KVStoreService, sk *stakingkeeper.Keeper, - slk slashingkeeper.Keeper, + slk SlashingKeeper, + bk BankKeeper, validatorAddressCodec addresscodec.Codec, logger log.Logger, ) Keeper { @@ -53,6 +58,7 @@ func NewKeeper( stakingKeeper: sk, validatorAddressCodec: validatorAddressCodec, slashKeeper: slk, + bankKeeper: bk, logger: logger, // Stores @@ -79,10 +85,14 @@ func (k Keeper) GetStakingKeeper() *stakingkeeper.Keeper { } // GetSlashingKeeper returns the slashing keeper. -func (k Keeper) GetSlashingKeeper() slashingkeeper.Keeper { +func (k Keeper) GetSlashingKeeper() SlashingKeeper { return k.slashKeeper } +func (k Keeper) GetBankKeeper() BankKeeper { + return k.bankKeeper +} + // GetAdmins returns the module's administrators with delegation of power control. func (k Keeper) GetAdmins(ctx context.Context) []string { p, err := k.GetParams(ctx) @@ -122,3 +132,47 @@ func (k Keeper) IsSenderValidator(ctx context.Context, sender string, expectedVa func (k Keeper) Logger() log.Logger { return k.logger } + +// updateBondedPoolPower updates the bonded pool to the correct power for the network. +func (k Keeper) UpdateBondedPoolPower(ctx context.Context) error { + newTotal := sdkmath.ZeroInt() + + del, err := k.stakingKeeper.GetAllDelegations(ctx) + if err != nil { + return err + } + + for _, d := range del { + newTotal = newTotal.Add(d.Shares.RoundInt()) + } + + bondDenom, err := k.stakingKeeper.BondDenom(ctx) + if err != nil { + return err + } + + prevBal := k.bankKeeper.GetBalance(ctx, authtypes.NewModuleAddress(stakingtypes.BondedPoolName), bondDenom).Amount + + if newTotal.Equal(prevBal) { + return nil + } + + if newTotal.GT(prevBal) { + diff := newTotal.Sub(prevBal) + coins := sdk.NewCoins(sdk.NewCoin(bondDenom, diff)) + + if err := k.bankKeeper.MintCoins(ctx, minttypes.ModuleName, coins); err != nil { + return err + } + + if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, minttypes.ModuleName, stakingtypes.BondedPoolName, coins); err != nil { + return err + } + } + + // no need to check if it goes down. When it does, it's automatic from the staking module as tokens are moved from + // bonded -> ToNotBonded pool. As PoA, we do not want any tokens in the ToNotBonded pool, so when a validator is removed + // they are slashed 100% (since it is PoA this is fine) which decreases the BondedPool balance, and leave NotBonded at 0. + + return nil +} diff --git a/keeper/keeper_test.go b/keeper/keeper_test.go index 6b41fac..b3e0487 100644 --- a/keeper/keeper_test.go +++ b/keeper/keeper_test.go @@ -91,7 +91,7 @@ func SetupTest(t *testing.T, baseValShares int64) *testFixture { registerBaseSDKModules(f, encCfg, storeService, logger, require) // Setup POA Keeper. - f.k = keeper.NewKeeper(encCfg.Codec, storeService, f.stakingKeeper, f.slashingKeeper, addresscodec.NewBech32Codec(sdk.Bech32PrefixValAddr), logger) + f.k = keeper.NewKeeper(encCfg.Codec, storeService, f.stakingKeeper, f.slashingKeeper, f.bankkeeper, addresscodec.NewBech32Codec(sdk.Bech32PrefixValAddr), logger) f.msgServer = keeper.NewMsgServerImpl(f.k) f.queryServer = keeper.NewQueryServerImpl(f.k) f.appModule = poamodule.NewAppModule(encCfg.Codec, f.k) diff --git a/keeper/msg_server.go b/keeper/msg_server.go index e9e5dfc..eab6fab 100644 --- a/keeper/msg_server.go +++ b/keeper/msg_server.go @@ -80,7 +80,7 @@ func (ms msgServer) SetPower(ctx context.Context, msg *poa.MsgSetPower) (*poa.Ms } } - return &poa.MsgSetPowerResponse{}, nil + return &poa.MsgSetPowerResponse{}, ms.k.UpdateBondedPoolPower(ctx) } func (ms msgServer) RemoveValidator(ctx context.Context, msg *poa.MsgRemoveValidator) (*poa.MsgRemoveValidatorResponse, error) { @@ -143,7 +143,7 @@ func (ms msgServer) RemoveValidator(ctx context.Context, msg *poa.MsgRemoveValid return nil, err } - return &poa.MsgRemoveValidatorResponse{}, nil + return &poa.MsgRemoveValidatorResponse{}, ms.k.UpdateBondedPoolPower(ctx) } func (ms msgServer) RemovePending(ctx context.Context, msg *poa.MsgRemovePending) (*poa.MsgRemovePendingResponse, error) { @@ -256,7 +256,7 @@ func (ms msgServer) CreateValidator(ctx context.Context, msg *poa.MsgCreateValid return nil, err } - return &poa.MsgCreateValidatorResponse{}, nil + return &poa.MsgCreateValidatorResponse{}, ms.k.UpdateBondedPoolPower(ctx) } // UpdateStakingParams wraps the x/staking module's UpdateStakingParams method so that only POA admins can invoke it. diff --git a/keeper/msg_server_test.go b/keeper/msg_server_test.go index dfa143f..b5d312b 100644 --- a/keeper/msg_server_test.go +++ b/keeper/msg_server_test.go @@ -7,10 +7,8 @@ import ( "github.com/stretchr/testify/require" sdk "github.com/cosmos/cosmos-sdk/types" - minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" - "github.com/cosmos/cosmos-sdk/x/staking/types" - "cosmossdk.io/math" + sdkmath "cosmossdk.io/math" "github.com/strangelove-ventures/poa" ) @@ -126,7 +124,7 @@ func TestSetPowerAndCreateValidator(t *testing.T) { vals, err := f.stakingKeeper.GetValidators(f.ctx, 100) require.NoError(err) - totalBonded := math.ZeroInt() + totalBonded := sdkmath.ZeroInt() for _, val := range vals { totalBonded = totalBonded.Add(val.GetBondedTokens()) } @@ -336,7 +334,7 @@ func TestRemoveValidator(t *testing.T) { isSelfRemovalAllowed: false, }, { - name: "remove validator as itself", + name: "success; remove validator as itself", request: &poa.MsgRemoveValidator{ Sender: sdk.AccAddress(MustValAddressFromBech32(vals[1].OperatorAddress)).String(), ValidatorAddress: vals[1].OperatorAddress, @@ -361,7 +359,7 @@ func TestRemoveValidator(t *testing.T) { currParams, _ := f.k.GetParams(f.ctx) currParams.AllowValidatorSelfExit = tc.isSelfRemovalAllowed - err := f.k.SetParams(f.ctx, currParams) + err = f.k.SetParams(f.ctx, currParams) require.NoError(err) _, err = f.msgServer.RemoveValidator(f.ctx, tc.request) @@ -372,15 +370,24 @@ func TestRemoveValidator(t *testing.T) { } else { require.NoError(err) - // This is only required in testing as we do not have a 'real' validator set - // signing blocks. - if err := f.MintTokensToBondedPool(t); err != nil { - panic(err) - } - - _, err := f.IncreaseBlock(5, true) + _, err := f.IncreaseBlock(3, true) require.NoError(err) } + + amt, err := f.stakingKeeper.TotalBondedTokens(f.ctx) + require.NoError(err) + require.True(amt.IsPositive()) + + notBondedPool := f.stakingKeeper.GetNotBondedPool(f.ctx) + bondDenom, err := f.stakingKeeper.BondDenom(f.ctx) + require.NoError(err) + bal := f.bankkeeper.GetBalance(f.ctx, notBondedPool.GetAddress(), bondDenom) + require.EqualValues(0, bal.Amount.Uint64()) + + // BondedRatio + bondRatio, err := f.stakingKeeper.BondedRatio(f.ctx) + require.NoError(err) + require.EqualValues(sdkmath.LegacyOneDec(), bondRatio) }) } } @@ -454,34 +461,3 @@ func TestMultipleUpdatesInASingleBlock(t *testing.T) { }) } } - -// mintTokensToBondedPool mints tokens to the bonded pool so the validator set -// in testing can be removed. -// In the future, this same logic would be run during the migration from POA->POS. -func (f *testFixture) MintTokensToBondedPool(t *testing.T) error { - t.Helper() - require := require.New(t) - - bondDenom, err := f.stakingKeeper.BondDenom(f.ctx) - require.NoError(err) - - validators, err := f.stakingKeeper.GetAllValidators(f.ctx) - require.NoError(err) - - amt := int64(0) - for _, v := range validators { - amt += v.GetBondedTokens().Int64() - } - - coins := sdk.NewCoins(sdk.NewCoin(bondDenom, math.NewInt(amt))) - - if err := f.bankkeeper.MintCoins(f.ctx, minttypes.ModuleName, coins); err != nil { - return err - } - - if err := f.bankkeeper.SendCoinsFromModuleToModule(f.ctx, minttypes.ModuleName, types.BondedPoolName, coins); err != nil { - return err - } - - return nil -} diff --git a/keeper/poa.go b/keeper/poa.go index a47c412..d1157c8 100644 --- a/keeper/poa.go +++ b/keeper/poa.go @@ -5,6 +5,7 @@ import ( "fmt" "math" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -16,13 +17,12 @@ import ( // UpdateValidatorSet updates a validator to their new share and consensus power, then updates the total power of the set. func (k Keeper) UpdateValidatorSet(ctx context.Context, newShares, newConsensusPower int64, val stakingtypes.Validator, valAddr sdk.ValAddress) error { - sdkContext := sdk.UnwrapSDKContext(ctx) - newShare := sdkmath.LegacyNewDec(newShares) newShareInt := sdkmath.NewIntFromUint64(uint64(newShares)) + delAddr := sdk.AccAddress(valAddr.Bytes()) delegation := stakingtypes.Delegation{ - DelegatorAddress: sdk.AccAddress(valAddr.Bytes()).String(), + DelegatorAddress: delAddr.String(), ValidatorAddress: val.OperatorAddress, Shares: newShare, } @@ -31,12 +31,6 @@ func (k Keeper) UpdateValidatorSet(ctx context.Context, newShares, newConsensusP return err } - // if we are removing a validator and it is not a gentx - // then we set the min self delegation +=1 so they unbond without slashing. - if newShares == 0 && sdkContext.BlockHeight() > 1 { - val.MinSelfDelegation = val.MinSelfDelegation.AddRaw(1) - } - val.Tokens = newShareInt val.DelegatorShares = newShare val.Status = stakingtypes.Bonded @@ -59,8 +53,8 @@ func (k Keeper) UpdateValidatorSet(ctx context.Context, newShares, newConsensusP // - updates the validator with the new shares, single delegation // - sets the last validator power to the new value. func (k Keeper) SetPOAPower(ctx context.Context, valOpBech32 string, newShares int64) (stakingtypes.Validator, error) { - powerReduction := k.stakingKeeper.PowerReduction(ctx) - newConsensusPower := newShares / powerReduction.Int64() + // 1 Consenus Power = 1_000_000 shares by default + newBFTConsensusPower := k.stakingKeeper.TokensToConsensusPower(ctx, sdkmath.NewInt(newShares)) valAddr, err := sdk.ValAddressFromBech32(valOpBech32) if err != nil { @@ -78,17 +72,33 @@ func (k Keeper) SetPOAPower(ctx context.Context, valOpBech32 string, newShares i return stakingtypes.Validator{}, err } + // slash all the validator's tokens (100%) + if newShares == 0 && currentPower > 0 { + pk, ok := val.ConsensusPubkey.GetCachedValue().(cryptotypes.PubKey) + if !ok { + return stakingtypes.Validator{}, fmt.Errorf("issue getting consensus pubkey for %s", valOpBech32) + } + + height := sdk.UnwrapSDKContext(ctx).BlockHeight() + + normalizedToken := k.stakingKeeper.TokensFromConsensusPower(ctx, currentPower) + + if _, err := k.stakingKeeper.Slash(ctx, sdk.GetConsAddress(pk), height, normalizedToken.Int64(), sdkmath.LegacyOneDec()); err != nil { + return stakingtypes.Validator{}, err + } + } + // Sets the new consensus power for the validator (this is executed in the x/staking ApplyAndReturnValidatorUpdates method) - if err := k.stakingKeeper.SetLastValidatorPower(ctx, valAddr, newConsensusPower); err != nil { + if err := k.stakingKeeper.SetLastValidatorPower(ctx, valAddr, newBFTConsensusPower); err != nil { return stakingtypes.Validator{}, err } - absPowerDiff := uint64(math.Abs(float64(newConsensusPower - currentPower))) + absPowerDiff := uint64(math.Abs(float64(newBFTConsensusPower - currentPower))) k.Logger().Debug("POA updatePOAPower", "valOpBech32", valOpBech32, "New Shares", newShares, - "New Consensus Power", newConsensusPower, + "New Consensus Power", newBFTConsensusPower, "Previous Power", currentPower, "absPowerDiff", absPowerDiff, ) @@ -97,7 +107,7 @@ func (k Keeper) SetPOAPower(ctx context.Context, valOpBech32 string, newShares i return stakingtypes.Validator{}, err } - if err := k.UpdateValidatorSet(ctx, newShares, newConsensusPower, val, valAddr); err != nil { + if err := k.UpdateValidatorSet(ctx, newShares, newBFTConsensusPower, val, valAddr); err != nil { return stakingtypes.Validator{}, err } @@ -140,7 +150,7 @@ func (k Keeper) AcceptNewValidator(ctx context.Context, operatingAddress string, ), }) - return nil + return k.UpdateBondedPoolPower(ctx) } // setValidatorInternals sets the validator's: @@ -185,5 +195,9 @@ func (k Keeper) updateTotalPower(ctx context.Context) error { // all tokens / 10^6 = new total power totalConsenusPower := allTokens.Quo(k.stakingKeeper.PowerReduction(ctx)) - return k.stakingKeeper.SetLastTotalPower(ctx, totalConsenusPower) + if err := k.stakingKeeper.SetLastTotalPower(ctx, totalConsenusPower); err != nil { + return err + } + + return k.UpdateBondedPoolPower(ctx) } diff --git a/module/depinject.go b/module/depinject.go index 899354d..4dfe8a7 100644 --- a/module/depinject.go +++ b/module/depinject.go @@ -4,6 +4,7 @@ import ( "os" "github.com/cosmos/cosmos-sdk/codec" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" @@ -41,6 +42,7 @@ type ModuleInputs struct { StakingKeeper stakingkeeper.Keeper SlashingKeeper slashingkeeper.Keeper + BankKeeper bankkeeper.Keeper } type ModuleOutputs struct { @@ -51,7 +53,7 @@ type ModuleOutputs struct { } func ProvideModule(in ModuleInputs) ModuleOutputs { - k := keeper.NewKeeper(in.Cdc, in.StoreService, &in.StakingKeeper, in.SlashingKeeper, in.AddressCodec, log.NewLogger(os.Stderr)) + k := keeper.NewKeeper(in.Cdc, in.StoreService, &in.StakingKeeper, in.SlashingKeeper, in.BankKeeper, in.AddressCodec, log.NewLogger(os.Stderr)) m := NewAppModule(in.Cdc, k) return ModuleOutputs{Module: m, Keeper: k, Out: depinject.Out{}} diff --git a/simapp/app.go b/simapp/app.go index 43bc8b0..54dd945 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -319,6 +319,7 @@ func NewSimApp( runtime.NewKVStoreService(keys[poatypes.StoreKey]), app.StakingKeeper, app.SlashingKeeper, + app.BankKeeper, authcodec.NewBech32Codec(sdk.Bech32PrefixValAddr), logger, ) @@ -450,16 +451,16 @@ func NewSimApp( distrtypes.ModuleName, slashingtypes.ModuleName, evidencetypes.ModuleName, - poa.ModuleName, stakingtypes.ModuleName, + poa.ModuleName, genutiltypes.ModuleName, authz.ModuleName, ) app.ModuleManager.SetOrderEndBlockers( crisistypes.ModuleName, govtypes.ModuleName, - poa.ModuleName, stakingtypes.ModuleName, + poa.ModuleName, genutiltypes.ModuleName, feegrant.ModuleName, group.ModuleName,