diff --git a/x/stake/pool.go b/x/stake/pool.go index df6407bd89..1e58fe28eb 100644 --- a/x/stake/pool.go +++ b/x/stake/pool.go @@ -60,7 +60,7 @@ func (p Pool) addTokensBonded(amount int64) (p2 Pool, issuedShares sdk.Rat) { func (p Pool) removeSharesBonded(shares sdk.Rat) (p2 Pool, removedTokens int64) { removedTokens = p.bondedShareExRate().Mul(shares).Evaluate() // (tokens/shares) * shares p.BondedShares = p.BondedShares.Sub(shares) - p.BondedPool -= removedTokens + p.BondedPool = p.BondedPool - removedTokens return p, removedTokens } diff --git a/x/stake/pool_test.go b/x/stake/pool_test.go index 8782889860..f8096e0ae2 100644 --- a/x/stake/pool_test.go +++ b/x/stake/pool_test.go @@ -4,7 +4,6 @@ import ( "fmt" "math/rand" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -12,6 +11,48 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +func TestBondedRatio(t *testing.T) { + ctx, _, keeper := createTestInput(t, nil, false, 0) + pool := keeper.GetPool(ctx) + pool.TotalSupply = 3 + pool.BondedPool = 2 + + // bonded pool / total supply + require.Equal(t, pool.bondedRatio(), sdk.NewRat(2).Quo(sdk.NewRat(3))) + pool.TotalSupply = 0 + + // avoids divide-by-zero + require.Equal(t, pool.bondedRatio(), sdk.ZeroRat) +} + +func TestBondedShareExRate(t *testing.T) { + ctx, _, keeper := createTestInput(t, nil, false, 0) + pool := keeper.GetPool(ctx) + pool.BondedPool = 3 + pool.BondedShares = sdk.NewRat(10) + + // bonded pool / bonded shares + require.Equal(t, pool.bondedShareExRate(), sdk.NewRat(3).Quo(sdk.NewRat(10))) + pool.BondedShares = sdk.ZeroRat + + // avoids divide-by-zero + require.Equal(t, pool.bondedShareExRate(), sdk.OneRat) +} + +func TestUnbondedShareExRate(t *testing.T) { + ctx, _, keeper := createTestInput(t, nil, false, 0) + pool := keeper.GetPool(ctx) + pool.UnbondedPool = 3 + pool.UnbondedShares = sdk.NewRat(10) + + // unbonded pool / unbonded shares + require.Equal(t, pool.unbondedShareExRate(), sdk.NewRat(3).Quo(sdk.NewRat(10))) + pool.UnbondedShares = sdk.ZeroRat + + // avoids divide-by-zero + require.Equal(t, pool.unbondedShareExRate(), sdk.OneRat) +} + func TestBondedToUnbondedPool(t *testing.T) { ctx, _, keeper := createTestInput(t, nil, false, 0) @@ -97,6 +138,7 @@ func TestRemoveSharesBonded(t *testing.T) { // same number of bonded shares / tokens when exchange rate is one assert.Equal(t, poolB.BondedShares, sdk.NewRat(poolB.BondedPool)) + } func TestAddTokensUnbonded(t *testing.T) { @@ -181,6 +223,35 @@ func TestCandidateRemoveShares(t *testing.T) { assert.Equal(t, candB.Assets, candA.Assets.Sub(sdk.NewRat(10).Mul(candA.delegatorShareExRate()))) // conservation of tokens assert.Equal(t, poolB.UnbondedPool+poolB.BondedPool+coinsB, poolA.UnbondedPool+poolA.BondedPool) + + // specific case from random tests + assets := sdk.NewRat(5102) + liabilities := sdk.NewRat(115) + cand := Candidate{ + Status: Bonded, + Address: addrs[0], + PubKey: pks[0], + Assets: assets, + Liabilities: liabilities, + } + pool := Pool{ + TotalSupply: 0, + BondedShares: sdk.NewRat(248305), + UnbondedShares: sdk.NewRat(232147), + BondedPool: 248305, + UnbondedPool: 232147, + InflationLastTime: 0, + Inflation: sdk.NewRat(7, 100), + } + shares := sdk.NewRat(29) + msg := fmt.Sprintf("candidate %s (status: %d, assets: %v, liabilities: %v, delegatorShareExRate: %v)", + cand.Address, cand.Status, cand.Assets, cand.Liabilities, cand.delegatorShareExRate()) + msg = fmt.Sprintf("Removed %v shares from %s", shares, msg) + newPool, _, tokens := pool.candidateRemoveShares(cand, shares) + require.Equal(t, + tokens+newPool.UnbondedPool+newPool.BondedPool, + pool.BondedPool+pool.UnbondedPool, + "Tokens were not conserved: %s", msg) } ///////////////////////////////////// @@ -206,7 +277,7 @@ func randomCandidate(r *rand.Rand) Candidate { } // generate a random staking state -func randomSetup(r *rand.Rand) (Pool, Candidate) { +func randomSetup(r *rand.Rand, numCandidates int) (Pool, Candidates) { pool := Pool{ TotalSupply: 0, BondedShares: sdk.ZeroRat, @@ -217,75 +288,75 @@ func randomSetup(r *rand.Rand) (Pool, Candidate) { Inflation: sdk.NewRat(7, 100), } - candidate := randomCandidate(r) - if candidate.Status == Bonded { - pool.BondedShares = pool.BondedShares.Add(candidate.Assets) - pool.BondedPool += candidate.Assets.Evaluate() - } else { - pool.UnbondedShares = pool.UnbondedShares.Add(candidate.Assets) - pool.UnbondedPool += candidate.Assets.Evaluate() + candidates := make([]Candidate, numCandidates) + for i := 0; i < numCandidates; i++ { + candidate := randomCandidate(r) + if candidate.Status == Bonded { + pool.BondedShares = pool.BondedShares.Add(candidate.Assets) + pool.BondedPool += candidate.Assets.Evaluate() + } else if candidate.Status == Unbonded { + pool.UnbondedShares = pool.UnbondedShares.Add(candidate.Assets) + pool.UnbondedPool += candidate.Assets.Evaluate() + } + candidates[i] = candidate } - return pool, candidate + return pool, candidates } -func randomTokens(r *rand.Rand) int64 { - return int64(r.Int31n(10000)) +// any operation that transforms staking state +// takes in RNG instance, pool, candidate +// returns updated pool, updated candidate, delta tokens, descriptive message +type Operation func(r *rand.Rand, p Pool, c Candidate) (Pool, Candidate, int64, string) + +// operation: bond or unbond a candidate depending on current status +func OpBondOrUnbond(r *rand.Rand, p Pool, cand Candidate) (Pool, Candidate, int64, string) { + var msg string + if cand.Status == Bonded { + msg = fmt.Sprintf("Unbonded previously bonded candidate %s (assets: %v, liabilities: %v, delegatorShareExRate: %v)", + cand.Address, cand.Assets, cand.Liabilities, cand.delegatorShareExRate()) + p, cand = p.bondedToUnbondedPool(cand) + + } else if cand.Status == Unbonded { + msg = fmt.Sprintf("Bonded previously unbonded candidate %s (assets: %v, liabilities: %v, delegatorShareExRate: %v)", + cand.Address, cand.Assets, cand.Liabilities, cand.delegatorShareExRate()) + p, cand = p.unbondedToBondedPool(cand) + } + return p, cand, 0, msg } -// operation that transforms staking state -type Operation func(p Pool, c Candidate) (Pool, Candidate, int64, string) +// operation: add a random number of tokens to a candidate +func OpAddTokens(r *rand.Rand, p Pool, cand Candidate) (Pool, Candidate, int64, string) { + tokens := int64(r.Int31n(1000)) + msg := fmt.Sprintf("candidate %s (status: %d, assets: %v, liabilities: %v, delegatorShareExRate: %v)", + cand.Address, cand.Status, cand.Assets, cand.Liabilities, cand.delegatorShareExRate()) + p, cand, _ = p.candidateAddTokens(cand, tokens) + msg = fmt.Sprintf("Added %d tokens to %s", tokens, msg) + return p, cand, -1 * tokens, msg // tokens are removed so for accounting must be negative +} + +// operation: remove a random number of shares from a candidate +func OpRemoveShares(r *rand.Rand, p Pool, cand Candidate) (Pool, Candidate, int64, string) { + var shares sdk.Rat + for { + shares = sdk.NewRat(int64(r.Int31n(1000))) + if shares.LT(cand.Liabilities) { + break + } + } + + msg := fmt.Sprintf("Removed %v shares from candidate %s (status: %d, assets: %v, liabilities: %v, delegatorShareExRate: %v)", + shares, cand.Address, cand.Status, cand.Assets, cand.Liabilities, cand.delegatorShareExRate()) + + p, cand, tokens := p.candidateRemoveShares(cand, shares) + return p, cand, tokens, msg +} // pick a random staking operation func randomOperation(r *rand.Rand) Operation { operations := []Operation{ - - // bond/unbond - func(p Pool, cand Candidate) (Pool, Candidate, int64, string) { - - var msg string - if cand.Status == Bonded { - msg = fmt.Sprintf("Unbonded previously bonded candidate %s (assets: %d, liabilities: %d, delegatorShareExRate: %v)", - cand.Address, cand.Assets.Evaluate(), cand.Liabilities.Evaluate(), cand.delegatorShareExRate()) - p, cand = p.bondedToUnbondedPool(cand) - } else { - msg = fmt.Sprintf("Bonded previously unbonded candidate %s (assets: %d, liabilities: %d, delegatorShareExRate: %v)", - cand.Address, cand.Assets.Evaluate(), cand.Liabilities.Evaluate(), cand.delegatorShareExRate()) - p, cand = p.unbondedToBondedPool(cand) - } - return p, cand, 0, msg - }, - - // add some tokens to a candidate - func(p Pool, cand Candidate) (Pool, Candidate, int64, string) { - - tokens := int64(r.Int31n(1000)) - - msg := fmt.Sprintf("candidate %s (assets: %d, liabilities: %d, delegatorShareExRate: %v)", - cand.Address, cand.Assets.Evaluate(), cand.Liabilities.Evaluate(), cand.delegatorShareExRate()) - - p, cand, _ = p.candidateAddTokens(cand, tokens) - - msg = fmt.Sprintf("Added %d tokens to %s", tokens, msg) - return p, cand, -1 * tokens, msg // tokens are removed so for accounting must be negative - }, - - // remove some shares from a candidate - func(p Pool, cand Candidate) (Pool, Candidate, int64, string) { - - shares := sdk.NewRat(int64(r.Int31n(1000))) - - if shares.GT(cand.Liabilities) { - shares = cand.Liabilities.Quo(sdk.NewRat(2)) - } - - msg := fmt.Sprintf("candidate %s (assets: %d, liabilities: %d, delegatorShareExRate: %v)", - cand.Address, cand.Assets.Evaluate(), cand.Liabilities.Evaluate(), cand.delegatorShareExRate()) - p, cand, tokens := p.candidateRemoveShares(cand, shares) - - msg = fmt.Sprintf("Removed %d shares from %s", shares.Evaluate(), msg) - - return p, cand, tokens, msg - }, + OpBondOrUnbond, + OpAddTokens, + OpRemoveShares, } r.Shuffle(len(operations), func(i, j int) { operations[i], operations[j] = operations[j], operations[i] @@ -295,80 +366,168 @@ func randomOperation(r *rand.Rand) Operation { // ensure invariants that should always be true are true func assertInvariants(t *testing.T, msg string, - pOrig Pool, cOrig Candidate, pMod Pool, cMod Candidate, tokens int64) { + pOrig Pool, cOrig Candidates, pMod Pool, cMods Candidates, tokens int64) { // total tokens conserved require.Equal(t, pOrig.UnbondedPool+pOrig.BondedPool, pMod.UnbondedPool+pMod.BondedPool+tokens, - "msg: %v\n, pOrig.UnbondedPool: %v, pOrig.BondedPool: %v, pMod.UnbondedPool: %v, pMod.BondedPool: %v, tokens: %v\n", + "Tokens not conserved - msg: %v\n, pOrig.BondedShares: %v, pOrig.UnbondedShares: %v, pMod.BondedShares: %v, pMod.UnbondedShares: %v, pOrig.UnbondedPool: %v, pOrig.BondedPool: %v, pMod.UnbondedPool: %v, pMod.BondedPool: %v, tokens: %v\n", msg, + pOrig.BondedShares, pOrig.UnbondedShares, + pMod.BondedShares, pMod.UnbondedShares, pOrig.UnbondedPool, pOrig.BondedPool, pMod.UnbondedPool, pMod.BondedPool, tokens) - // nonnegative shares + // nonnegative bonded shares require.False(t, pMod.BondedShares.LT(sdk.ZeroRat), - "msg: %v\n, pOrig: %v\n, pMod: %v\n, cOrig: %v\n, cMod %v, tokens: %v\n", - msg, pOrig, pMod, cOrig, cMod, tokens) - require.False(t, pMod.UnbondedShares.LT(sdk.ZeroRat), - "msg: %v\n, pOrig: %v\n, pMod: %v\n, cOrig: %v\n, cMod %v, tokens: %v\n", - msg, pOrig, pMod, cOrig, cMod, tokens) + "Negative bonded shares - msg: %v\npOrig: %#v\npMod: %#v\ntokens: %v\n", + msg, pOrig, pMod, tokens) - // nonnegative ex rates + // nonnegative unbonded shares + require.False(t, pMod.UnbondedShares.LT(sdk.ZeroRat), + "Negative unbonded shares - msg: %v\npOrig: %#v\npMod: %#v\ntokens: %v\n", + msg, pOrig, pMod, tokens) + + // nonnegative bonded ex rate require.False(t, pMod.bondedShareExRate().LT(sdk.ZeroRat), "Applying operation \"%s\" resulted in negative bondedShareExRate: %d", msg, pMod.bondedShareExRate().Evaluate()) + // nonnegative unbonded ex rate require.False(t, pMod.unbondedShareExRate().LT(sdk.ZeroRat), "Applying operation \"%s\" resulted in negative unbondedShareExRate: %d", msg, pMod.unbondedShareExRate().Evaluate()) - // nonnegative ex rate - require.False(t, cMod.delegatorShareExRate().LT(sdk.ZeroRat), - "Applying operation \"%s\" resulted in negative candidate.delegatorShareExRate(): %v (candidate.PubKey: %s)", - msg, - cMod.delegatorShareExRate(), - cMod.PubKey, - ) + for _, cMod := range cMods { - // nonnegative assets / liabilities - require.False(t, cMod.Assets.LT(sdk.ZeroRat), - "Applying operation \"%s\" resulted in negative candidate.Assets: %d (candidate.Liabilities: %d, candidate.PubKey: %s)", - msg, - cMod.Assets.Evaluate(), - cMod.Liabilities.Evaluate(), - cMod.PubKey, - ) + // nonnegative ex rate + require.False(t, cMod.delegatorShareExRate().LT(sdk.ZeroRat), + "Applying operation \"%s\" resulted in negative candidate.delegatorShareExRate(): %v (candidate.Address: %s)", + msg, + cMod.delegatorShareExRate(), + cMod.Address, + ) + + // nonnegative assets + require.False(t, cMod.Assets.LT(sdk.ZeroRat), + "Applying operation \"%s\" resulted in negative candidate.Assets: %v (candidate.Liabilities: %v, candidate.delegatorShareExRate: %v, candidate.Address: %s)", + msg, + cMod.Assets, + cMod.Liabilities, + cMod.delegatorShareExRate(), + cMod.Address, + ) + + // nonnegative liabilities + require.False(t, cMod.Liabilities.LT(sdk.ZeroRat), + "Applying operation \"%s\" resulted in negative candidate.Liabilities: %v (candidate.Assets: %v, candidate.delegatorShareExRate: %v, candidate.Address: %s)", + msg, + cMod.Liabilities, + cMod.Assets, + cMod.delegatorShareExRate(), + cMod.Address, + ) + + } - require.False(t, cMod.Liabilities.LT(sdk.ZeroRat), - "Applying operation \"%s\" resulted in negative candidate.Liabilities: %d (candidate.Assets: %d, candidate.PubKey: %s)", - msg, - cMod.Liabilities.Evaluate(), - cMod.Assets.Evaluate(), - cMod.PubKey, - ) } -// run random operations in a random order on a random state, assert invariants hold -func TestIntegrationInvariants(t *testing.T) { +// TODO Re-enable once the overflow bug is fixed! +// ref https://github.com/cosmos/cosmos-sdk/issues/753 +/* +func TestPossibleOverflow(t *testing.T) { + assets := sdk.NewRat(2159) + liabilities := sdk.NewRat(391432570689183511).Quo(sdk.NewRat(40113011844664)) + cand := Candidate{ + Status: Bonded, + Address: addrs[0], + PubKey: pks[0], + Assets: assets, + Liabilities: liabilities, + } + pool := Pool{ + TotalSupply: 0, + BondedShares: assets, + UnbondedShares: sdk.ZeroRat, + BondedPool: assets.Evaluate(), + UnbondedPool: 0, + InflationLastTime: 0, + Inflation: sdk.NewRat(7, 100), + } + tokens := int64(71) + msg := fmt.Sprintf("candidate %s (status: %d, assets: %v, liabilities: %v, delegatorShareExRate: %v)", + cand.Address, cand.Status, cand.Assets, cand.Liabilities, cand.delegatorShareExRate()) + _, newCandidate, _ := pool.candidateAddTokens(cand, tokens) + + msg = fmt.Sprintf("Added %d tokens to %s", tokens, msg) + require.False(t, newCandidate.delegatorShareExRate().LT(sdk.ZeroRat), + "Applying operation \"%s\" resulted in negative delegatorShareExRate(): %v", + msg, newCandidate.delegatorShareExRate()) +} +*/ + +// run random operations in a random order on a random single-candidate state, assert invariants hold +func TestSingleCandidateIntegrationInvariants(t *testing.T) { + r := rand.New(rand.NewSource(41)) + for i := 0; i < 10; i++ { + poolOrig, candidatesOrig := randomSetup(r, 1) + require.Equal(t, 1, len(candidatesOrig)) - r1 := rand.New(rand.NewSource(time.Now().UnixNano())) - pool, candidates := randomSetup(r1) - initialPool, initialCandidates := pool, candidates - + // sanity check assertInvariants(t, "no operation", - initialPool, initialCandidates, - pool, candidates, 0) + poolOrig, candidatesOrig, + poolOrig, candidatesOrig, 0) - for j := 0; j < 100; j++ { + // TODO Increase iteration count once overflow bug is fixed + // ref https://github.com/cosmos/cosmos-sdk/issues/753 + for j := 0; j < 4; j++ { + poolMod, candidateMod, tokens, msg := randomOperation(r)(r, poolOrig, candidatesOrig[0]) - r2 := rand.New(rand.NewSource(time.Now().UnixNano())) - pool, candidates, tokens, msg := randomOperation(r2)(pool, candidates) + candidatesMod := make([]Candidate, len(candidatesOrig)) + copy(candidatesMod[:], candidatesOrig[:]) + require.Equal(t, 1, len(candidatesOrig), "j %v", j) + require.Equal(t, 1, len(candidatesMod), "j %v", j) + candidatesMod[0] = candidateMod assertInvariants(t, msg, - initialPool, initialCandidates, - pool, candidates, tokens) + poolOrig, candidatesOrig, + poolMod, candidatesMod, tokens) + + poolOrig = poolMod + candidatesOrig = candidatesMod + } + } +} + +// run random operations in a random order on a random multi-candidate state, assert invariants hold +func TestMultiCandidateIntegrationInvariants(t *testing.T) { + r := rand.New(rand.NewSource(42)) + + for i := 0; i < 10; i++ { + poolOrig, candidatesOrig := randomSetup(r, 100) + + assertInvariants(t, "no operation", + poolOrig, candidatesOrig, + poolOrig, candidatesOrig, 0) + + // TODO Increase iteration count once overflow bug is fixed + // ref https://github.com/cosmos/cosmos-sdk/issues/753 + for j := 0; j < 3; j++ { + index := int(r.Int31n(int32(len(candidatesOrig)))) + poolMod, candidateMod, tokens, msg := randomOperation(r)(r, poolOrig, candidatesOrig[index]) + candidatesMod := make([]Candidate, len(candidatesOrig)) + copy(candidatesMod[:], candidatesOrig[:]) + candidatesMod[index] = candidateMod + + assertInvariants(t, msg, + poolOrig, candidatesOrig, + poolMod, candidatesMod, tokens) + + poolOrig = poolMod + candidatesOrig = candidatesMod + } } } diff --git a/x/stake/types.go b/x/stake/types.go index 7e7fb9e753..a5d85bc7ae 100644 --- a/x/stake/types.go +++ b/x/stake/types.go @@ -104,7 +104,7 @@ func (c Candidate) delegatorShareExRate() sdk.Rat { // Should only be called when the Candidate qualifies as a validator. func (c Candidate) validator() Validator { return Validator{ - Address: c.Address, // XXX !!! + Address: c.Address, VotingPower: c.Assets, } }