cosmos-sdk/x/stake/pool_test.go

302 lines
13 KiB
Go
Raw Normal View History

2018-03-22 09:00:45 -07:00
package stake
2018-03-28 11:35:25 -07:00
import (
"fmt"
2018-03-28 11:35:25 -07:00
"math/rand"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
2018-03-28 11:35:25 -07:00
sdk "github.com/cosmos/cosmos-sdk/types"
crypto "github.com/tendermint/go-crypto"
)
func TestBondedToUnbondedPool(t *testing.T) {
ctx, _, keeper := createTestInput(t, nil, false, 0)
poolA := keeper.GetPool(ctx)
assert.Equal(t, poolA.bondedShareExRate(), sdk.OneRat)
assert.Equal(t, poolA.unbondedShareExRate(), sdk.OneRat)
candA := candidate1
poolB, candB := poolA.bondedToUnbondedPool(candA)
// status unbonded
assert.Equal(t, candB.Status, Unbonded)
// same exchange rate, assets unchanged
assert.Equal(t, candB.Assets, candA.Assets)
// bonded pool decreased
assert.Equal(t, poolB.BondedPool, poolA.BondedPool-candA.Assets.Evaluate())
// unbonded pool increased
assert.Equal(t, poolB.UnbondedPool, poolA.UnbondedPool+candA.Assets.Evaluate())
// conservation of tokens
assert.Equal(t, poolB.UnbondedPool+poolB.BondedPool, poolA.BondedPool+poolA.UnbondedPool)
}
func TestUnbonbedtoBondedPool(t *testing.T) {
ctx, _, keeper := createTestInput(t, nil, false, 0)
poolA := keeper.GetPool(ctx)
assert.Equal(t, poolA.bondedShareExRate(), sdk.OneRat)
assert.Equal(t, poolA.unbondedShareExRate(), sdk.OneRat)
candA := candidate1
candA.Status = Unbonded
poolB, candB := poolA.unbondedToBondedPool(candA)
// status bonded
assert.Equal(t, candB.Status, Bonded)
// same exchange rate, assets unchanged
assert.Equal(t, candB.Assets, candA.Assets)
// bonded pool increased
assert.Equal(t, poolB.BondedPool, poolA.BondedPool+candA.Assets.Evaluate())
// unbonded pool decreased
assert.Equal(t, poolB.UnbondedPool, poolA.UnbondedPool-candA.Assets.Evaluate())
// conservation of tokens
assert.Equal(t, poolB.UnbondedPool+poolB.BondedPool, poolA.BondedPool+poolA.UnbondedPool)
}
func TestAddTokensBonded(t *testing.T) {
ctx, _, keeper := createTestInput(t, nil, false, 0)
poolA := keeper.GetPool(ctx)
assert.Equal(t, poolA.bondedShareExRate(), sdk.OneRat)
poolB, sharesB := poolA.addTokensBonded(10)
assert.Equal(t, poolB.bondedShareExRate(), sdk.OneRat)
// correct changes to bonded shares and bonded pool
assert.Equal(t, poolB.BondedShares, poolA.BondedShares.Add(sharesB))
assert.Equal(t, poolB.BondedPool, poolA.BondedPool+10)
// same number of bonded shares / tokens when exchange rate is one
assert.Equal(t, poolB.BondedShares, sdk.NewRat(poolB.BondedPool))
}
func TestRemoveSharesBonded(t *testing.T) {
ctx, _, keeper := createTestInput(t, nil, false, 0)
poolA := keeper.GetPool(ctx)
assert.Equal(t, poolA.bondedShareExRate(), sdk.OneRat)
poolB, tokensB := poolA.removeSharesBonded(sdk.NewRat(10))
assert.Equal(t, poolB.bondedShareExRate(), sdk.OneRat)
// correct changes to bonded shares and bonded pool
assert.Equal(t, poolB.BondedShares, poolA.BondedShares.Sub(sdk.NewRat(10)))
assert.Equal(t, poolB.BondedPool, poolA.BondedPool-tokensB)
// 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) {
ctx, _, keeper := createTestInput(t, nil, false, 0)
poolA := keeper.GetPool(ctx)
assert.Equal(t, poolA.unbondedShareExRate(), sdk.OneRat)
poolB, sharesB := poolA.addTokensUnbonded(10)
assert.Equal(t, poolB.unbondedShareExRate(), sdk.OneRat)
// correct changes to unbonded shares and unbonded pool
assert.Equal(t, poolB.UnbondedShares, poolA.UnbondedShares.Add(sharesB))
assert.Equal(t, poolB.UnbondedPool, poolA.UnbondedPool+10)
// same number of unbonded shares / tokens when exchange rate is one
assert.Equal(t, poolB.UnbondedShares, sdk.NewRat(poolB.UnbondedPool))
}
func TestRemoveSharesUnbonded(t *testing.T) {
ctx, _, keeper := createTestInput(t, nil, false, 0)
poolA := keeper.GetPool(ctx)
assert.Equal(t, poolA.unbondedShareExRate(), sdk.OneRat)
poolB, tokensB := poolA.removeSharesUnbonded(sdk.NewRat(10))
assert.Equal(t, poolB.unbondedShareExRate(), sdk.OneRat)
// correct changes to unbonded shares and bonded pool
assert.Equal(t, poolB.UnbondedShares, poolA.UnbondedShares.Sub(sdk.NewRat(10)))
assert.Equal(t, poolB.UnbondedPool, poolA.UnbondedPool-tokensB)
// same number of unbonded shares / tokens when exchange rate is one
assert.Equal(t, poolB.UnbondedShares, sdk.NewRat(poolB.UnbondedPool))
}
func TestCandidateAddTokens(t *testing.T) {
ctx, _, keeper := createTestInput(t, nil, false, 0)
poolA := keeper.GetPool(ctx)
candA := Candidate{
Address: addrVal1,
PubKey: pk1,
Assets: sdk.NewRat(9),
Liabilities: sdk.NewRat(9),
Status: Bonded,
}
poolA.BondedPool = candA.Assets.Evaluate()
poolA.BondedShares = candA.Assets
assert.Equal(t, candA.delegatorShareExRate(), sdk.OneRat)
assert.Equal(t, poolA.bondedShareExRate(), sdk.OneRat)
assert.Equal(t, poolA.unbondedShareExRate(), sdk.OneRat)
poolB, candB, sharesB := poolA.candidateAddTokens(candA, 10)
// shares were issued
assert.Equal(t, sharesB, sdk.NewRat(10).Mul(candA.delegatorShareExRate()))
// pool shares were added
assert.Equal(t, candB.Assets, candA.Assets.Add(sdk.NewRat(10)))
// conservation of tokens
assert.Equal(t, poolB.UnbondedPool+poolB.BondedPool, 10+poolA.UnbondedPool+poolA.BondedPool)
}
func TestCandidateRemoveShares(t *testing.T) {
ctx, _, keeper := createTestInput(t, nil, false, 0)
poolA := keeper.GetPool(ctx)
candA := Candidate{
Address: addrVal1,
PubKey: pk1,
Assets: sdk.NewRat(9),
Liabilities: sdk.NewRat(9),
Status: Bonded,
}
poolA.BondedPool = candA.Assets.Evaluate()
poolA.BondedShares = candA.Assets
assert.Equal(t, candA.delegatorShareExRate(), sdk.OneRat)
assert.Equal(t, poolA.bondedShareExRate(), sdk.OneRat)
assert.Equal(t, poolA.unbondedShareExRate(), sdk.OneRat)
poolB, candB, coinsB := poolA.candidateRemoveShares(candA, sdk.NewRat(10))
// coins were created
assert.Equal(t, coinsB, int64(10))
// pool shares were removed
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)
}
// generate a random candidate
func randomCandidate(r *rand.Rand) Candidate {
var status CandidateStatus
if r.Float64() < float64(0.5) {
status = Bonded
} else {
status = Unbonded
}
address := testAddr("A58856F0FD53BF058B4909A21AEC019107BA6160")
pubkey := crypto.GenPrivKeyEd25519().PubKey()
assets := sdk.NewRat(int64(r.Int31n(10000)))
liabilities := sdk.NewRat(int64(r.Int31n(10000)))
return Candidate{
Status: status,
Address: address,
PubKey: pubkey,
Assets: assets,
Liabilities: liabilities,
}
}
// generate a random staking state
func randomSetup(r *rand.Rand) (Pool, Candidates, int64) {
pool := Pool{
TotalSupply: 0,
BondedShares: sdk.ZeroRat,
UnbondedShares: sdk.ZeroRat,
BondedPool: 0,
UnbondedPool: 0,
InflationLastTime: 0,
Inflation: sdk.NewRat(7, 100),
}
var candidates []Candidate
for i := int32(0); i < r.Int31n(1000); i++ {
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 = append(candidates, candidate)
}
tokens := int64(r.Int31n(10000))
return pool, candidates, tokens
}
// operation that transforms staking state
type Operation func(p Pool, c Candidates, t int64) (Pool, Candidates, int64, string)
2018-03-28 11:35:25 -07:00
// pick a random staking operation
func randomOperation(r *rand.Rand) Operation {
operations := []Operation{
// bond/unbond
func(p Pool, c Candidates, t int64) (Pool, Candidates, int64, string) {
2018-03-28 11:35:25 -07:00
index := int(r.Int31n(int32(len(c))))
cand := c[index]
var msg string
2018-03-28 11:35:25 -07:00
if cand.Status == Bonded {
msg = fmt.Sprintf("Unbonded previously bonded candidate %s (assets: %d, liabilities: %d, delegatorShareExRate: %d)", cand.PubKey, cand.Assets.Evaluate(), cand.Liabilities.Evaluate(), cand.delegatorShareExRate().Evaluate())
2018-03-28 11:35:25 -07:00
p, cand = p.bondedToUnbondedPool(cand)
cand.Status = Unbonded
} else {
msg = fmt.Sprintf("Bonded previously unbonded candidate %s (assets: %d, liabilities: %d, delegatorShareExRate: %d)", cand.PubKey, cand.Assets.Evaluate(), cand.Liabilities.Evaluate(), cand.delegatorShareExRate().Evaluate())
2018-03-28 11:35:25 -07:00
p, cand = p.unbondedToBondedPool(cand)
cand.Status = Bonded
}
c[index] = cand
return p, c, t, msg
2018-03-28 11:35:25 -07:00
},
// add some tokens to a candidate
func(p Pool, c Candidates, t int64) (Pool, Candidates, int64, string) {
2018-03-28 11:35:25 -07:00
tokens := int64(r.Int31n(1000))
index := int(r.Int31n(int32(len(c))))
cand := c[index]
msg := fmt.Sprintf("candidate with pubkey %s, %d assets, %d liabilities, and %d delegatorShareExRate", cand.PubKey, cand.Assets.Evaluate(), cand.Liabilities.Evaluate(), cand.delegatorShareExRate().Evaluate())
2018-03-28 11:35:25 -07:00
p, cand, _ = p.candidateAddTokens(cand, tokens)
c[index] = cand
t -= tokens
msg = fmt.Sprintf("Added %d tokens to %s", tokens, msg)
return p, c, t, msg
2018-03-28 11:35:25 -07:00
},
// remove some shares from a candidate
func(p Pool, c Candidates, t int64) (Pool, Candidates, int64, string) {
shares := sdk.NewRat(int64(r.Int31n(1000)))
index := int(r.Int31n(int32(len(c))))
cand := c[index]
if shares.GT(cand.Liabilities) {
shares = cand.Liabilities.Quo(sdk.NewRat(2))
}
msg := fmt.Sprintf("candidate with pubkey %s, %d assets, %d liabilities, and %d delegatorShareExRate", cand.PubKey, cand.Assets.Evaluate(), cand.Liabilities.Evaluate(), cand.delegatorShareExRate().Evaluate())
p, cand, tokens := p.candidateRemoveShares(cand, shares)
c[index] = cand
t += tokens
msg = fmt.Sprintf("Removed %d shares from %s", shares.Evaluate(), msg)
return p, c, t, msg
},
2018-03-28 11:35:25 -07:00
}
r.Shuffle(len(operations), func(i, j int) {
operations[i], operations[j] = operations[j], operations[i]
})
return operations[0]
}
// ensure invariants that should always be true are true
func assertInvariants(t *testing.T, pA Pool, cA Candidates, tA int64, pB Pool, cB Candidates, tB int64, msg string) {
2018-03-28 11:35:25 -07:00
// total tokens conserved
require.Equal(t, pA.UnbondedPool+pA.BondedPool+tA, pB.UnbondedPool+pB.BondedPool+tB)
2018-03-28 11:35:25 -07:00
// nonnegative shares
require.Equal(t, pB.BondedShares.LT(sdk.ZeroRat), false)
require.Equal(t, pB.UnbondedShares.LT(sdk.ZeroRat), false)
// nonnegative ex rates
require.Equal(t, pB.bondedShareExRate().LT(sdk.ZeroRat), false, "Applying operation \"%s\" resulted in negative bondedShareExRate: %d", msg, pB.bondedShareExRate().Evaluate())
require.Equal(t, pB.unbondedShareExRate().LT(sdk.ZeroRat), false, "Applying operation \"%s\" resulted in negative unbondedShareExRate: %d", msg, pB.unbondedShareExRate().Evaluate())
2018-03-28 11:35:25 -07:00
bondedSharesHeld := sdk.ZeroRat
unbondedSharesHeld := sdk.ZeroRat
for _, candidate := range cA {
// nonnegative ex rate
require.Equal(t, false, candidate.delegatorShareExRate().LT(sdk.ZeroRat), "Applying operation \"%s\" resulted in negative candidate.delegatorShareExRate(): %s (candidate.PubKey: %s)", msg, candidate.delegatorShareExRate(), candidate.PubKey)
2018-03-28 11:35:25 -07:00
// nonnegative assets / liabilities
require.Equal(t, false, candidate.Assets.LT(sdk.ZeroRat), "Applying operation \"%s\" resulted in negative candidate.Assets: %d (candidate.Liabilities: %d, candidate.PubKey: %s)", msg, candidate.Assets.Evaluate(), candidate.Liabilities.Evaluate(), candidate.PubKey)
require.Equal(t, false, candidate.Liabilities.LT(sdk.ZeroRat), "Applying operation \"%s\" resulted in negative candidate.Liabilities: %d (candidate.Assets: %d, candidate.PubKey: %s)", msg, candidate.Liabilities.Evaluate(), candidate.Assets.Evaluate(), candidate.PubKey)
2018-03-28 11:35:25 -07:00
if candidate.Status == Bonded {
bondedSharesHeld = bondedSharesHeld.Add(candidate.Assets)
} else {
unbondedSharesHeld = unbondedSharesHeld.Add(candidate.Assets)
}
}
// shares outstanding = total shares held by candidates, both bonded and unbonded
require.Equal(t, bondedSharesHeld, pB.BondedShares)
require.Equal(t, unbondedSharesHeld, pB.UnbondedShares)
2018-03-28 11:35:25 -07:00
}
// run random operations in a random order on a random state, assert invariants hold
func TestIntegrationInvariants(t *testing.T) {
r := rand.New(rand.NewSource(int64(42)))
var msg string
2018-03-28 11:35:25 -07:00
for i := 0; i < 10; i++ {
pool, candidates, tokens := randomSetup(r)
initialPool, initialCandidates, initialTokens := pool, candidates, tokens
assertInvariants(t, initialPool, initialCandidates, initialTokens, pool, candidates, tokens, "NOOP")
2018-03-28 11:35:25 -07:00
for j := 0; j < 100; j++ {
pool, candidates, tokens, msg = randomOperation(r)(pool, candidates, tokens)
assertInvariants(t, initialPool, initialCandidates, initialTokens, pool, candidates, tokens, msg)
2018-03-28 11:35:25 -07:00
}
}
}