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

R4R: Allow the undelegation of more coins than were delegated; More validity checks. #3717

Merged
merged 15 commits into from
Feb 27, 2019
6 changes: 4 additions & 2 deletions docs/spec/auth/05_vesting.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,12 @@ func DelegateCoins(t Time, from Account, amount Coins) {
### Undelegating

For a vesting account attempting to undelegate `D` coins, the following is performed:
NOTE: `DV < D` and `(DV + DF) < D` may be possible due to quirks in the rounding of
delegation/undelegation logic.

1. Verify `(DV + DF) >= D > 0` (this is simply a sanity check)
1. Verify `D > 0`
2. Compute `X := min(DF, D)` (portion of `D` that should become free, prioritizing free coins)
3. Compute `Y := D - X` (portion of `D` that should remain vesting)
3. Compute `Y := min(DV, D - X)` (portion of `D` that should remain vesting)
4. Set `DF -= X`
5. Set `DV -= Y`
6. Set `BC += D`
Expand Down
2 changes: 0 additions & 2 deletions types/coin.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,6 @@ func (coins Coins) AmountOf(denom string) Int {

// IsAllPositive returns true if there is at least one coin and all currencies
// have a positive value.
//
// TODO: Remove once unsigned integers are used.
func (coins Coins) IsAllPositive() bool {
if len(coins) == 0 {
return false
Expand Down
5 changes: 3 additions & 2 deletions x/auth/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,12 +295,13 @@ func (bva *BaseVestingAccount) TrackUndelegation(amount sdk.Coins) {
panic("undelegation attempt with zero coins")
}
delegatedFree := bva.DelegatedFree.AmountOf(coin.Denom)
delegatedVesting := bva.DelegatedVesting.AmountOf(coin.Denom)

// compute x and y per the specification, where:
// X := min(DF, D)
// Y := D - X
// Y := min(D - X, DV)
x := sdk.MinInt(delegatedFree, coin.Amount)
y := coin.Amount.Sub(x)
y := sdk.MinInt(delegatedVesting, coin.Amount.Sub(x))

if !x.IsZero() {
xCoin := sdk.NewCoin(coin.Denom, x)
Expand Down
74 changes: 63 additions & 11 deletions x/bank/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ func NewBaseKeeper(ak auth.AccountKeeper,
}

// SetCoins sets the coins at the addr.
func (keeper BaseKeeper) SetCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) sdk.Error {
func (keeper BaseKeeper) SetCoins(
ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins,
) sdk.Error {

if !amt.IsValid() {
return sdk.ErrInvalidCoins(amt.String())
}
return setCoins(ctx, keeper.ak, addr, amt)
}

Expand All @@ -56,6 +62,9 @@ func (keeper BaseKeeper) SubtractCoins(
ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins,
) (sdk.Coins, sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, nil, sdk.ErrInvalidCoins(amt.String())
}
return subtractCoins(ctx, keeper.ak, addr, amt)
}

Expand All @@ -64,6 +73,9 @@ func (keeper BaseKeeper) AddCoins(
ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins,
) (sdk.Coins, sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, nil, sdk.ErrInvalidCoins(amt.String())
}
return addCoins(ctx, keeper.ak, addr, amt)
}

Expand All @@ -78,14 +90,27 @@ func (keeper BaseKeeper) InputOutputCoins(
// DelegateCoins performs delegation by deducting amt coins from an account with
// address addr. For vesting accounts, delegations amounts are tracked for both
// vesting and vested coins.
func (keeper BaseKeeper) DelegateCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) {
func (keeper BaseKeeper) DelegateCoins(
ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins,
) (sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, sdk.ErrInvalidCoins(amt.String())
}
return delegateCoins(ctx, keeper.ak, addr, amt)
}

// UndelegateCoins performs undelegation by crediting amt coins to an account with
// address addr. For vesting accounts, undelegation amounts are tracked for both
// vesting and vested coins.
func (keeper BaseKeeper) UndelegateCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) {
// If any of the undelegation amounts are negative, an error is returned.
func (keeper BaseKeeper) UndelegateCoins(
ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins,
) (sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, sdk.ErrInvalidCoins(amt.String())
}
return undelegateCoins(ctx, keeper.ak, addr, amt)
}

Expand Down Expand Up @@ -126,6 +151,10 @@ func NewBaseSendKeeper(ak auth.AccountKeeper,
func (keeper BaseSendKeeper) SendCoins(
ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins,
) (sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, sdk.ErrInvalidCoins(amt.String())
}
return sendCoins(ctx, keeper.ak, fromAddr, toAddr, amt)
}

Expand Down Expand Up @@ -188,6 +217,9 @@ func getCoins(ctx sdk.Context, am auth.AccountKeeper, addr sdk.AccAddress) sdk.C
}

func setCoins(ctx sdk.Context, am auth.AccountKeeper, addr sdk.AccAddress, amt sdk.Coins) sdk.Error {
if !amt.IsValid() {
return sdk.ErrInvalidCoins(amt.String())
}
acc := am.GetAccount(ctx, addr)
if acc == nil {
acc = am.NewAccountWithAddress(ctx, addr)
Expand Down Expand Up @@ -218,6 +250,11 @@ func setAccount(ctx sdk.Context, ak auth.AccountKeeper, acc auth.Account) {
//
// CONTRACT: If the account is a vesting account, the amount has to be spendable.
func subtractCoins(ctx sdk.Context, ak auth.AccountKeeper, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, nil, sdk.ErrInvalidCoins(amt.String())
}

oldCoins, spendableCoins := sdk.Coins{}, sdk.Coins{}

acc := getAccount(ctx, ak, addr)
Expand All @@ -244,6 +281,11 @@ func subtractCoins(ctx sdk.Context, ak auth.AccountKeeper, addr sdk.AccAddress,

// AddCoins adds amt to the coins at the addr.
func addCoins(ctx sdk.Context, am auth.AccountKeeper, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, nil, sdk.ErrInvalidCoins(amt.String())
}

oldCoins := getCoins(ctx, am, addr)
newCoins := oldCoins.Add(amt)

Expand All @@ -260,9 +302,9 @@ func addCoins(ctx sdk.Context, am auth.AccountKeeper, addr sdk.AccAddress, amt s
}

// SendCoins moves coins from one account to another
// Returns ErrInvalidCoins if amt is invalid.
func sendCoins(ctx sdk.Context, am auth.AccountKeeper, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) {
// Safety check ensuring that when sending coins the keeper must maintain the
// supply invariant.
if !amt.IsValid() {
return nil, sdk.ErrInvalidCoins(amt.String())
}
Expand All @@ -284,7 +326,7 @@ func sendCoins(ctx sdk.Context, am auth.AccountKeeper, fromAddr sdk.AccAddress,
// NOTE: Make sure to revert state changes from tx on error
func inputOutputCoins(ctx sdk.Context, am auth.AccountKeeper, inputs []Input, outputs []Output) (sdk.Tags, sdk.Error) {
// Safety check ensuring that when sending coins the keeper must maintain the
// supply invariant.
// Check supply invariant and validity of Coins.
if err := ValidateInputsOutputs(inputs, outputs); err != nil {
return nil, err
}
Expand Down Expand Up @@ -314,6 +356,10 @@ func delegateCoins(
ctx sdk.Context, ak auth.AccountKeeper, addr sdk.AccAddress, amt sdk.Coins,
) (sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, sdk.ErrInvalidCoins(amt.String())
}

acc := getAccount(ctx, ak, addr)
if acc == nil {
return nil, sdk.ErrUnknownAddress(fmt.Sprintf("account %s does not exist", addr))
Expand Down Expand Up @@ -344,6 +390,10 @@ func undelegateCoins(
ctx sdk.Context, ak auth.AccountKeeper, addr sdk.AccAddress, amt sdk.Coins,
) (sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, sdk.ErrInvalidCoins(amt.String())
}

acc := getAccount(ctx, ak, addr)
if acc == nil {
return nil, sdk.ErrUnknownAddress(fmt.Sprintf("account %s does not exist", addr))
Expand All @@ -361,22 +411,24 @@ func undelegateCoins(
), nil
}

func trackDelegation(acc auth.Account, blockTime time.Time, amount sdk.Coins) error {
// CONTRACT: assumes that amt is valid.
func trackDelegation(acc auth.Account, blockTime time.Time, amt sdk.Coins) error {
vacc, ok := acc.(auth.VestingAccount)
if ok {
vacc.TrackDelegation(blockTime, amount)
vacc.TrackDelegation(blockTime, amt)
return nil
}

return acc.SetCoins(acc.GetCoins().Sub(amount))
return acc.SetCoins(acc.GetCoins().Sub(amt))
}

func trackUndelegation(acc auth.Account, amount sdk.Coins) error {
// CONTRACT: assumes that amt is valid.
func trackUndelegation(acc auth.Account, amt sdk.Coins) error {
vacc, ok := acc.(auth.VestingAccount)
if ok {
vacc.TrackUndelegation(amount)
vacc.TrackUndelegation(amt)
return nil
}

return acc.SetCoins(acc.GetCoins().Add(amount))
return acc.SetCoins(acc.GetCoins().Add(amt))
}
3 changes: 3 additions & 0 deletions x/bank/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ func (msg MsgSend) ValidateBasic() sdk.Error {
if msg.ToAddress.Empty() {
return sdk.ErrInvalidAddress("missing recipient address")
}
if !msg.Amount.IsValid() {
return sdk.ErrInvalidCoins("send amount is invalid: " + msg.Amount.String())
}
if !msg.Amount.IsAllPositive() {
return sdk.ErrInsufficientCoins("send amount must be positive")
}
Expand Down
8 changes: 5 additions & 3 deletions x/distribution/keeper/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,11 @@ func (k Keeper) withdrawDelegationRewards(ctx sdk.Context, val sdk.Validator, de
k.SetFeePool(ctx, feePool)

// add coins to user account
withdrawAddr := k.GetDelegatorWithdrawAddr(ctx, del.GetDelegatorAddr())
if _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, coins); err != nil {
return err
if !coins.IsZero() {
withdrawAddr := k.GetDelegatorWithdrawAddr(ctx, del.GetDelegatorAddr())
if _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, coins); err != nil {
return err
}
}

// remove delegator starting info
Expand Down
10 changes: 6 additions & 4 deletions x/distribution/keeper/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ func (h Hooks) AfterValidatorRemoved(ctx sdk.Context, _ sdk.ConsAddress, valAddr
h.k.SetOutstandingRewards(ctx, outstanding.Sub(commission))

// add to validator account
accAddr := sdk.AccAddress(valAddr)
withdrawAddr := h.k.GetDelegatorWithdrawAddr(ctx, accAddr)
if !coins.IsZero() {
accAddr := sdk.AccAddress(valAddr)
withdrawAddr := h.k.GetDelegatorWithdrawAddr(ctx, accAddr)

if _, _, err := h.k.bankKeeper.AddCoins(ctx, withdrawAddr, coins); err != nil {
panic(err)
if _, _, err := h.k.bankKeeper.AddCoins(ctx, withdrawAddr, coins); err != nil {
panic(err)
}
}
}
// remove commission record
Expand Down
10 changes: 6 additions & 4 deletions x/distribution/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,13 @@ func (k Keeper) WithdrawValidatorCommission(ctx sdk.Context, valAddr sdk.ValAddr
outstanding := k.GetOutstandingRewards(ctx)
k.SetOutstandingRewards(ctx, outstanding.Sub(sdk.NewDecCoins(coins)))

accAddr := sdk.AccAddress(valAddr)
withdrawAddr := k.GetDelegatorWithdrawAddr(ctx, accAddr)
if !coins.IsZero() {
accAddr := sdk.AccAddress(valAddr)
withdrawAddr := k.GetDelegatorWithdrawAddr(ctx, accAddr)

if _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, coins); err != nil {
return err
if _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, coins); err != nil {
return err
}
}

return nil
Expand Down
1 change: 1 addition & 0 deletions x/ibc/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func handleIBCReceiveMsg(ctx sdk.Context, ibcm Mapper, ck BankKeeper, msg IBCRec
return ErrInvalidSequence(ibcm.codespace).Result()
}

// XXX Check that packet.Coins is valid and positive (nonzero)
_, _, err := ck.AddCoins(ctx, packet.DestAddr, packet.Coins)
if err != nil {
return err.Result()
Expand Down
30 changes: 27 additions & 3 deletions x/mock/simulation/rand_util.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package simulation

import (
"fmt"
"math/big"
"math/rand"
"time"
Expand Down Expand Up @@ -37,14 +38,37 @@ func RandStringOfLength(r *rand.Rand, n int) string {
}

// Generate a random amount
// Note: The range of RandomAmount includes max, and is, in fact, biased to return max.
func RandomAmount(r *rand.Rand, max sdk.Int) sdk.Int {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func RandomAmount(r *rand.Rand, max sdk.Int) sdk.Int {
func BiasedRandomAmount(r *rand.Rand, max sdk.Int) sdk.Int {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessary to differentiate... RandomAmount might as well be biased all the time intelligently...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO it's not intuitive. Can the same be said for the native go library's rand functions?

return sdk.NewInt(int64(r.Intn(int(max.Int64()))))
// return sdk.NewInt(int64(r.Intn(int(max.Int64()))))
max2 := big.NewInt(0).Mul(max.BigInt(), big.NewInt(2))
randInt := big.NewInt(0).Rand(r, max2)
if randInt.Cmp(max.BigInt()) > 0 {
randInt = max.BigInt()
}
result := sdk.NewIntFromBigInt(randInt)
// Sanity
if result.GT(max) {
panic(fmt.Sprintf("%v > %v", result, max))
}
return result
}

// RandomDecAmount generates a random decimal amount
// Note: The range of RandomDecAmount includes max, and is, in fact, biased to return max.
func RandomDecAmount(r *rand.Rand, max sdk.Dec) sdk.Dec {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func RandomDecAmount(r *rand.Rand, max sdk.Dec) sdk.Dec {
func BiasedRandomDecAmount(r *rand.Rand, max sdk.Dec) sdk.Dec {

randInt := big.NewInt(0).Rand(r, max.Int)
return sdk.NewDecFromBigIntWithPrec(randInt, sdk.Precision)
// randInt := big.NewInt(0).Rand(r, max.Int)
max2 := big.NewInt(0).Mul(max.Int, big.NewInt(2))
randInt := big.NewInt(0).Rand(r, max2)
if randInt.Cmp(max.Int) > 0 {
randInt = max.Int
}
result := sdk.NewDecFromBigIntWithPrec(randInt, sdk.Precision)
// Sanity
if result.GT(max) {
panic(fmt.Sprintf("%v > %v", result, max))
}
return result
}

// RandomSetGenesis wraps mock.RandomSetGenesis, but using simulation accounts
Expand Down
11 changes: 10 additions & 1 deletion x/slashing/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,16 @@ func (k Keeper) handleDoubleSign(ctx sdk.Context, addr crypto.Address, infractio
consAddr := sdk.ConsAddress(addr)
pubkey, err := k.getPubkey(ctx, addr)
if err != nil {
panic(fmt.Sprintf("Validator consensus-address %v not found", consAddr))
// ignore evidence that cannot be handled.
return
// NOTE:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't talking about having the validators in storage here, we're talking about just the public keys associated with the consensus addresses. Why would Tendermint ever send the application a consensus address for which the application hadn't stored a public key?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's in the comment below. because the app deletes the validator after the unbonding period is over (say), and the simulator or tendermint doesn't yet know exactly when this happens. so currently the simulator is breaking the app because it's not very intelligent with inserting evidence. same could happen w/ tendermint.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed the app does delete the validator after the unbonding period is over (x/slashing/hooks.go:35), and the simulation could still send evidence - which I would consider a bug in the simulation, but I guess the simulation's job is to model whatever the actual Tendermint might do - does Tendermint submit evidence past the unbonding period (if max evidence age is set equal to it)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem likely that programmers will get it all right, even if Tendermint were to support evidence... it just seems like a difficult thing to do, or even to prove from the perspective of a developer, even if the criterion for evidence correctness were to be defined by the app in another ABCI message (say). So let's go with defensive here, which we can undo later easily (since it's a single if statement). Later apps can upgrade to become less permissive, and that's fine once Tendermint+SDK's been upgraded to support evidence at only allowable heights.

Notifying the programmer/operator of these issues can be done via WARN logging/notification of some sort, which should be built out on the Tendermint side.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put all documentation above the return statement.

// We used to panic with:
// `panic(fmt.Sprintf("Validator consensus-address %v not found", consAddr))`,
// but this couples the expectations of the app to both Tendermint and
// the simulator. Both are expected to provide the full range of
// allowable but none of the disallowed evidence types. Instead of
// getting this coordination right, it is easier to relax the
// constraints and ignore evidence that cannot be handled.
}

// Reject evidence if the double-sign is too old
Expand Down
4 changes: 2 additions & 2 deletions x/staking/keeper/delegation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func TestUnbondDelegation(t *testing.T) {
}

func TestUnbondingDelegationsMaxEntries(t *testing.T) {
ctx, _, keeper := CreateTestInput(t, false, 0)
ctx, _, keeper := CreateTestInput(t, false, 1)
pool := keeper.GetPool(ctx)
startTokens := sdk.TokensFromTendermintPower(10)
pool.NotBondedTokens = startTokens
Expand Down Expand Up @@ -372,7 +372,7 @@ func TestUndelegateFromUnbondingValidator(t *testing.T) {
}

func TestUndelegateFromUnbondedValidator(t *testing.T) {
ctx, _, keeper := CreateTestInput(t, false, 0)
ctx, _, keeper := CreateTestInput(t, false, 1)
pool := keeper.GetPool(ctx)
startTokens := sdk.TokensFromTendermintPower(20)
pool.NotBondedTokens = startTokens
Expand Down
4 changes: 2 additions & 2 deletions x/staking/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ func TestPool(t *testing.T) {

//check that the empty keeper loads the default
resPool := keeper.GetPool(ctx)
require.True(t, expPool.Equal(resPool))
require.Equal(t, expPool, resPool)

//modify a params, save, and retrieve
expPool.BondedTokens = sdk.NewInt(777)
keeper.SetPool(ctx, expPool)
resPool = keeper.GetPool(ctx)
require.True(t, expPool.Equal(resPool))
require.Equal(t, expPool, resPool)
}
Loading