From 67123a3a4642d62235e79b917feb970a015729eb Mon Sep 17 00:00:00 2001 From: rigelrozanski Date: Sat, 12 May 2018 18:54:50 -0400 Subject: [PATCH] move validator property fn and tests from pool.go to validator.go --- x/stake/handler.go | 6 +- x/stake/keeper.go | 2 +- x/stake/keeper_keys.go | 2 +- x/stake/pool.go | 115 ----------- x/stake/pool_test.go | 423 ++------------------------------------ x/stake/tick_test.go | 10 +- x/stake/validator.go | 117 ++++++++++- x/stake/validator_test.go | 420 +++++++++++++++++++++++++++++++++++++ 8 files changed, 558 insertions(+), 537 deletions(-) create mode 100644 x/stake/validator_test.go diff --git a/x/stake/handler.go b/x/stake/handler.go index 3d0b23359..aac654f2b 100644 --- a/x/stake/handler.go +++ b/x/stake/handler.go @@ -162,7 +162,7 @@ func delegate(ctx sdk.Context, k Keeper, delegatorAddr sdk.Address, if err != nil { return nil, err } - pool, validator, newShares := pool.validatorAddTokens(validator, bondAmt.Amount) + validator, pool, newShares := validator.addTokens(pool, bondAmt.Amount) bond.Shares = bond.Shares.Add(newShares) // Update bond height @@ -244,7 +244,7 @@ func handleMsgUnbond(ctx sdk.Context, msg MsgUnbond, k Keeper) sdk.Result { // Add the coins p := k.GetPool(ctx) - p, validator, returnAmount := p.validatorRemoveShares(validator, shares) + validator, p, returnAmount := validator.removeShares(p, shares) returnCoins := sdk.Coins{{k.GetParams(ctx).BondDenom, returnAmount}} k.coinKeeper.AddCoins(ctx, bond.DelegatorAddr, returnCoins) @@ -256,7 +256,7 @@ func handleMsgUnbond(ctx sdk.Context, msg MsgUnbond, k Keeper) sdk.Result { // change the share types to unbonded if they were not already if validator.Status == sdk.Bonded { validator.Status = sdk.Unbonded - p, validator = p.UpdateSharesLocation(validator) + validator, p = validator.UpdateSharesLocation(p) } // lastly update the status diff --git a/x/stake/keeper.go b/x/stake/keeper.go index 5e178be30..2bb120f5d 100644 --- a/x/stake/keeper.go +++ b/x/stake/keeper.go @@ -126,7 +126,7 @@ func (k Keeper) setValidator(ctx sdk.Context, validator Validator) { if oldValidator.Status != sdk.Bonded && valIsNowBonded { validator.Status = sdk.Bonded - pool, validator = pool.UpdateSharesLocation(validator) + validator, pool = validator.UpdateSharesLocation(pool) k.setPool(ctx, pool) } diff --git a/x/stake/keeper_keys.go b/x/stake/keeper_keys.go index eef0ce98a..92e9bfe4b 100644 --- a/x/stake/keeper_keys.go +++ b/x/stake/keeper_keys.go @@ -33,7 +33,7 @@ func GetValidatorKey(addr sdk.Address) []byte { // get the key for the validator used in the power-store func GetValidatorsBondedByPowerKey(validator Validator, pool Pool) []byte { - power := pool.EquivalentBondedShares(validator) + power := validator.EquivalentBondedShares(pool) powerBytes := []byte(power.ToLeftPadded(maxDigitsForAccount)) // power big-endian (more powerful validators first) // TODO ensure that the key will be a readable string.. probably should add seperators and have diff --git a/x/stake/pool.go b/x/stake/pool.go index 7abaa7ae8..8cb62ad6d 100644 --- a/x/stake/pool.go +++ b/x/stake/pool.go @@ -128,118 +128,3 @@ func (p Pool) removeSharesUnbonded(shares sdk.Rat) (p2 Pool, removedTokens int64 p.UnbondedTokens -= removedTokens return p, removedTokens } - -//_______________________________________________________________________ -// Validator Properties TODO move back to types.go under validator (maybe split types.go) - -// XXX write test -// update the location of the shares within a validator if its bond status has changed -func (p Pool) UpdateSharesLocation(validator Validator) (Pool, Validator) { - var tokens int64 - - switch { - case !validator.BondedShares.IsZero(): - if validator.Status == sdk.Bonded { // return if nothing needs switching - return p, validator - } - p, tokens = p.removeSharesBonded(validator.BondedShares) - case !validator.UnbondingShares.IsZero(): - if validator.Status == sdk.Unbonding { - return p, validator - } - p, tokens = p.removeSharesUnbonding(validator.BondedShares) - case !validator.UnbondedShares.IsZero(): - if validator.Status == sdk.Unbonding { - return p, validator - } - p, tokens = p.removeSharesUnbonded(validator.BondedShares) - } - - switch validator.Status { - case sdk.Bonded: - p, validator.BondedShares = p.addTokensBonded(tokens) - case sdk.Unbonding: - p, validator.UnbondingShares = p.addTokensUnbonding(tokens) - case sdk.Unbonded, sdk.Revoked: - p, validator.UnbondedShares = p.addTokensUnbonded(tokens) - } - - return p, validator -} - -// XXX TEST -// get the power or potential power for a validator -// if bonded, the power is the BondedShares -// if not bonded, the power is the amount of bonded shares which the -// the validator would have it was bonded -func (p Pool) EquivalentBondedShares(validator Validator) (power sdk.Rat) { - switch validator.Status { - case sdk.Bonded: - power = validator.BondedShares - case sdk.Unbonding: - shares := validator.UnbondingShares // ubShr - exRate := p.unbondingShareExRate().Quo(p.bondedShareExRate()) // (tok/ubshr)/(tok/bshr) = bshr/ubshr - power = shares.Mul(exRate) // ubshr*bshr/ubshr = bshr - case sdk.Unbonded, sdk.Revoked: - shares := validator.UnbondedShares // ubShr - exRate := p.unbondedShareExRate().Quo(p.bondedShareExRate()) // (tok/ubshr)/(tok/bshr) = bshr/ubshr - power = shares.Mul(exRate) // ubshr*bshr/ubshr = bshr - } - return -} - -// get the equivalent amount of tokens contained by a validator -func (p Pool) validatorTokens(v Validator) sdk.Rat { - switch v.Status { - case sdk.Bonded: - return p.unbondedShareExRate().Mul(v.BondedShares) // (tokens/shares) * shares - case sdk.Unbonding: - return p.unbondedShareExRate().Mul(v.UnbondingShares) - case sdk.Unbonded, sdk.Revoked: - return p.unbondedShareExRate().Mul(v.UnbondedShares) - } - return sdk.ZeroRat() -} - -// XXX Audit this function further to make sure it's correct -// add tokens to a validator -func (p Pool) validatorAddTokens(validator Validator, - amount int64) (p2 Pool, validator2 Validator, issuedDelegatorShares sdk.Rat) { - - var poolShares sdk.Rat - switch validator.Status { - case sdk.Bonded: - p, poolShares = p.addTokensBonded(amount) - validator.BondedShares = validator.BondedShares.Add(poolShares) - case sdk.Unbonding: - p, poolShares = p.addTokensUnbonding(amount) - validator.UnbondingShares = validator.UnbondingShares.Add(poolShares) - case sdk.Unbonded, sdk.Revoked: - p, poolShares = p.addTokensUnbonded(amount) - validator.UnbondedShares = validator.UnbondedShares.Add(poolShares) - } - - equivalentBondedShares := p.EquivalentBondedShares(validator) - exRate := validator.DelegatorShareExRate(p) // eq-val-bonded-shares/delegator-shares - issuedDelegatorShares = equivalentBondedShares.Quo(exRate) - validator.DelegatorShares = validator.DelegatorShares.Add(issuedDelegatorShares) - - return p, validator, issuedDelegatorShares -} - -// remove shares from a validator -func (p Pool) validatorRemoveShares(validator Validator, - shares sdk.Rat) (p2 Pool, validator2 Validator, createdCoins int64) { - - //exRate := validator.DelegatorShareExRate() //XXX make sure not used - - globalPoolSharesToRemove := validator.DelegatorShareExRate(p).Mul(shares) - if validator.Status == sdk.Bonded { - p, createdCoins = p.removeSharesBonded(globalPoolSharesToRemove) - } else { - p, createdCoins = p.removeSharesUnbonded(globalPoolSharesToRemove) - } - validator.BondedShares = validator.BondedShares.Sub(globalPoolSharesToRemove) - validator.DelegatorShares = validator.DelegatorShares.Sub(shares) - return p, validator, createdCoins -} diff --git a/x/stake/pool_test.go b/x/stake/pool_test.go index 5813847c6..c031940ee 100644 --- a/x/stake/pool_test.go +++ b/x/stake/pool_test.go @@ -1,8 +1,6 @@ package stake import ( - "fmt" - "math/rand" "testing" "github.com/stretchr/testify/assert" @@ -39,6 +37,20 @@ func TestBondedShareExRate(t *testing.T) { require.Equal(t, pool.bondedShareExRate(), sdk.OneRat()) } +func TestUnbondingShareExRate(t *testing.T) { + ctx, _, keeper := createTestInput(t, false, 0) + pool := keeper.GetPool(ctx) + pool.UnbondingPool = 3 + pool.UnbondingShares = sdk.NewRat(10) + + // unbonding pool / unbonding shares + require.Equal(t, pool.unbondingShareExRate(), sdk.NewRat(3).Quo(sdk.NewRat(10))) + pool.UnbondingShares = sdk.ZeroRat() + + // avoids divide-by-zero + require.Equal(t, pool.unbondingShareExRate(), sdk.OneRat()) +} + func TestUnbondedShareExRate(t *testing.T) { ctx, _, keeper := createTestInput(t, false, 0) pool := keeper.GetPool(ctx) @@ -53,62 +65,6 @@ func TestUnbondedShareExRate(t *testing.T) { require.Equal(t, pool.unbondedShareExRate(), sdk.OneRat()) } -// TODO convert these commend out tests to test UpdateSharesLocation -//func TestBondedToUnbondedPool(t *testing.T) { -//ctx, _, keeper := createTestInput(t, false, 0) - -//poolA := keeper.GetPool(ctx) -//assert.Equal(t, poolA.bondedShareExRate(), sdk.OneRat()) -//assert.Equal(t, poolA.unbondedShareExRate(), sdk.OneRat()) -//valA := Validator{ -//Status: sdk.Bonded, -//Address: addrs[0], -//PubKey: pks[0], -//BondedShares: sdk.OneRat(), -//DelegatorShares: sdk.OneRat(), -//} -//poolB, valB := poolA.bondedToUnbondedPool(valA) - -//// status unbonded -//assert.Equal(t, valB.Status, sdk.Unbonded) -//// same exchange rate, assets unchanged -//assert.Equal(t, valB.BondedShares, valA.BondedShares) -//// bonded pool decreased -//assert.Equal(t, poolB.BondedPool, poolA.BondedPool-valA.BondedShares.Evaluate()) -//// unbonded pool increased -//assert.Equal(t, poolB.UnbondedPool, poolA.UnbondedPool+valA.BondedShares.Evaluate()) -//// conservation of tokens -//assert.Equal(t, poolB.UnbondedPool+poolB.BondedPool, poolA.BondedPool+poolA.UnbondedPool) -//} - -//func TestUnbonbedtoBondedPool(t *testing.T) { -//ctx, _, keeper := createTestInput(t, false, 0) - -//poolA := keeper.GetPool(ctx) -//assert.Equal(t, poolA.bondedShareExRate(), sdk.OneRat()) -//assert.Equal(t, poolA.unbondedShareExRate(), sdk.OneRat()) -//valA := Validator{ -//Status: sdk.Bonded, -//Address: addrs[0], -//PubKey: pks[0], -//BondedShares: sdk.OneRat(), -//DelegatorShares: sdk.OneRat(), -//} -//valA.Status = sdk.Unbonded -//poolB, valB := poolA.unbondedToBondedPool(valA) - -//// status bonded -//assert.Equal(t, valB.Status, sdk.Bonded) -//// same exchange rate, assets unchanged -//assert.Equal(t, valB.BondedShares, valA.BondedShares) -//// bonded pool increased -//assert.Equal(t, poolB.BondedPool, poolA.BondedPool+valA.BondedShares.Evaluate()) -//// unbonded pool decreased -//assert.Equal(t, poolB.UnbondedPool, poolA.UnbondedPool-valA.BondedShares.Evaluate()) -//// conservation of tokens -//assert.Equal(t, poolB.UnbondedPool+poolB.BondedPool, poolA.BondedPool+poolA.UnbondedPool) -//} - func TestAddTokensBonded(t *testing.T) { ctx, _, keeper := createTestInput(t, false, 0) @@ -172,354 +128,3 @@ func TestRemoveSharesUnbonded(t *testing.T) { // same number of unbonded shares / tokens when exchange rate is one assert.True(t, poolB.UnbondedShares.Equal(sdk.NewRat(poolB.UnbondedPool))) } - -func TestValidatorAddTokens(t *testing.T) { - ctx, _, keeper := createTestInput(t, false, 0) - - poolA := keeper.GetPool(ctx) - valA := Validator{ - Status: sdk.Bonded, - Address: addrs[0], - PubKey: pks[0], - BondedShares: sdk.NewRat(9), - DelegatorShares: sdk.NewRat(9), - } - poolA.BondedPool = valA.BondedShares.Evaluate() - poolA.BondedShares = valA.BondedShares - assert.Equal(t, valA.DelegatorShareExRate(), sdk.OneRat()) - assert.Equal(t, poolA.bondedShareExRate(), sdk.OneRat()) - assert.Equal(t, poolA.unbondedShareExRate(), sdk.OneRat()) - poolB, valB, sharesB := poolA.validatorAddTokens(valA, 10) - - // shares were issued - assert.Equal(t, sdk.NewRat(10).Mul(valA.DelegatorShareExRate()), sharesB) - // pool shares were added - assert.Equal(t, valB.BondedShares, valA.BondedShares.Add(sdk.NewRat(10))) - // conservation of tokens - assert.Equal(t, poolB.BondedPool, 10+poolA.BondedPool) -} - -func TestValidatorRemoveShares(t *testing.T) { - ctx, _, keeper := createTestInput(t, false, 0) - - poolA := keeper.GetPool(ctx) - valA := Validator{ - Status: sdk.Bonded, - Address: addrs[0], - PubKey: pks[0], - BondedShares: sdk.NewRat(9), - DelegatorShares: sdk.NewRat(9), - } - poolA.BondedPool = valA.BondedShares.Evaluate() - poolA.BondedShares = valA.BondedShares - assert.Equal(t, valA.DelegatorShareExRate(), sdk.OneRat()) - assert.Equal(t, poolA.bondedShareExRate(), sdk.OneRat()) - assert.Equal(t, poolA.unbondedShareExRate(), sdk.OneRat()) - poolB, valB, coinsB := poolA.validatorRemoveShares(valA, sdk.NewRat(10)) - - // coins were created - assert.Equal(t, coinsB, int64(10)) - // pool shares were removed - assert.Equal(t, valB.BondedShares, valA.BondedShares.Sub(sdk.NewRat(10).Mul(valA.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) - val := Validator{ - Status: sdk.Bonded, - Address: addrs[0], - PubKey: pks[0], - BondedShares: assets, - DelegatorShares: 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("validator %s (status: %d, assets: %v, liabilities: %v, DelegatorShareExRate: %v)", - val.Address, val.Status, val.BondedShares, val.DelegatorShares, val.DelegatorShareExRate()) - msg = fmt.Sprintf("Removed %v shares from %s", shares, msg) - newPool, _, tokens := pool.validatorRemoveShares(val, shares) - require.Equal(t, - tokens+newPool.UnbondedPool+newPool.BondedPool, - pool.BondedPool+pool.UnbondedPool, - "Tokens were not conserved: %s", msg) -} - -//________________________________________________________________________________ -// TODO refactor this random setup - -// generate a random validator -func randomValidator(r *rand.Rand) Validator { - var status sdk.BondStatus - if r.Float64() < float64(0.5) { - status = sdk.Bonded - } else { - status = sdk.Unbonded - } - assets := sdk.NewRat(int64(r.Int31n(10000))) - liabilities := sdk.NewRat(int64(r.Int31n(10000))) - return Validator{ - Status: status, - Address: addrs[0], - PubKey: pks[0], - BondedShares: assets, - DelegatorShares: liabilities, - } -} - -// generate a random staking state -func randomSetup(r *rand.Rand, numValidators int) (Pool, Validators) { - pool := Pool{ - TotalSupply: 0, - BondedShares: sdk.ZeroRat(), - UnbondedShares: sdk.ZeroRat(), - BondedPool: 0, - UnbondedPool: 0, - InflationLastTime: 0, - Inflation: sdk.NewRat(7, 100), - } - - validators := make([]Validator, numValidators) - for i := 0; i < numValidators; i++ { - validator := randomValidator(r) - if validator.Status == sdk.Bonded { - pool.BondedShares = pool.BondedShares.Add(validator.BondedShares) - pool.BondedPool += validator.BondedShares.Evaluate() - } else if validator.Status == sdk.Unbonded { - pool.UnbondedShares = pool.UnbondedShares.Add(validator.BondedShares) - pool.UnbondedPool += validator.BondedShares.Evaluate() - } - validators[i] = validator - } - return pool, validators -} - -// any operation that transforms staking state -// takes in RNG instance, pool, validator -// returns updated pool, updated validator, delta tokens, descriptive message -type Operation func(r *rand.Rand, p Pool, c Validator) (Pool, Validator, int64, string) - -// operation: bond or unbond a validator depending on current status -func OpBondOrUnbond(r *rand.Rand, p Pool, val Validator) (Pool, Validator, int64, string) { - var msg string - if val.Status == sdk.Bonded { - msg = fmt.Sprintf("sdk.Unbonded previously bonded validator %s (assets: %v, liabilities: %v, DelegatorShareExRate: %v)", - val.Address, val.BondedShares, val.DelegatorShares, val.DelegatorShareExRate()) - val.Status = sdk.Unbonded - } else if val.Status == sdk.Unbonded { - msg = fmt.Sprintf("sdk.Bonded previously unbonded validator %s (assets: %v, liabilities: %v, DelegatorShareExRate: %v)", - val.Address, val.BondedShares, val.DelegatorShares, val.DelegatorShareExRate()) - val.Status = sdk.Bonded - } - p, val = p.UpdateSharesLocation(val) - return p, val, 0, msg -} - -// operation: add a random number of tokens to a validator -func OpAddTokens(r *rand.Rand, p Pool, val Validator) (Pool, Validator, int64, string) { - tokens := int64(r.Int31n(1000)) - msg := fmt.Sprintf("validator %s (status: %d, assets: %v, liabilities: %v, DelegatorShareExRate: %v)", - val.Address, val.Status, val.BondedShares, val.DelegatorShares, val.DelegatorShareExRate()) - p, val, _ = p.validatorAddTokens(val, tokens) - msg = fmt.Sprintf("Added %d tokens to %s", tokens, msg) - return p, val, -1 * tokens, msg // tokens are removed so for accounting must be negative -} - -// operation: remove a random number of shares from a validator -func OpRemoveShares(r *rand.Rand, p Pool, val Validator) (Pool, Validator, int64, string) { - var shares sdk.Rat - for { - shares = sdk.NewRat(int64(r.Int31n(1000))) - if shares.LT(val.DelegatorShares) { - break - } - } - - msg := fmt.Sprintf("Removed %v shares from validator %s (status: %d, assets: %v, liabilities: %v, DelegatorShareExRate: %v)", - shares, val.Address, val.Status, val.BondedShares, val.DelegatorShares, val.DelegatorShareExRate()) - - p, val, tokens := p.validatorRemoveShares(val, shares) - return p, val, tokens, msg -} - -// pick a random staking operation -func randomOperation(r *rand.Rand) Operation { - operations := []Operation{ - OpBondOrUnbond, - OpAddTokens, - OpRemoveShares, - } - 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, msg string, - pOrig Pool, cOrig Validators, pMod Pool, cMods Validators, tokens int64) { - - // total tokens conserved - require.Equal(t, - pOrig.UnbondedPool+pOrig.BondedPool, - pMod.UnbondedPool+pMod.BondedPool+tokens, - "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 bonded shares - require.False(t, pMod.BondedShares.LT(sdk.ZeroRat()), - "Negative bonded shares - msg: %v\npOrig: %#v\npMod: %#v\ntokens: %v\n", - msg, pOrig, pMod, tokens) - - // 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()) - - for _, cMod := range cMods { - - // nonnegative ex rate - require.False(t, cMod.DelegatorShareExRate().LT(sdk.ZeroRat()), - "Applying operation \"%s\" resulted in negative validator.DelegatorShareExRate(): %v (validator.Address: %s)", - msg, - cMod.DelegatorShareExRate(), - cMod.Address, - ) - - // nonnegative assets - require.False(t, cMod.BondedShares.LT(sdk.ZeroRat()), - "Applying operation \"%s\" resulted in negative validator.BondedShares: %v (validator.DelegatorShares: %v, validator.DelegatorShareExRate: %v, validator.Address: %s)", - msg, - cMod.BondedShares, - cMod.DelegatorShares, - cMod.DelegatorShareExRate(), - cMod.Address, - ) - - // nonnegative liabilities - require.False(t, cMod.DelegatorShares.LT(sdk.ZeroRat()), - "Applying operation \"%s\" resulted in negative validator.DelegatorShares: %v (validator.BondedShares: %v, validator.DelegatorShareExRate: %v, validator.Address: %s)", - msg, - cMod.DelegatorShares, - cMod.BondedShares, - cMod.DelegatorShareExRate(), - cMod.Address, - ) - - } - -} - -func TestPossibleOverflow(t *testing.T) { - assets := sdk.NewRat(2159) - liabilities := sdk.NewRat(391432570689183511).Quo(sdk.NewRat(40113011844664)) - val := Validator{ - Status: sdk.Bonded, - Address: addrs[0], - PubKey: pks[0], - BondedShares: assets, - DelegatorShares: 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("validator %s (status: %d, assets: %v, liabilities: %v, DelegatorShareExRate: %v)", - val.Address, val.Status, val.BondedShares, val.DelegatorShares, val.DelegatorShareExRate()) - _, newValidator, _ := pool.validatorAddTokens(val, tokens) - - msg = fmt.Sprintf("Added %d tokens to %s", tokens, msg) - require.False(t, newValidator.DelegatorShareExRate().LT(sdk.ZeroRat()), - "Applying operation \"%s\" resulted in negative DelegatorShareExRate(): %v", - msg, newValidator.DelegatorShareExRate()) -} - -// run random operations in a random order on a random single-validator state, assert invariants hold -func TestSingleValidatorIntegrationInvariants(t *testing.T) { - r := rand.New(rand.NewSource(41)) - - for i := 0; i < 10; i++ { - poolOrig, validatorsOrig := randomSetup(r, 1) - require.Equal(t, 1, len(validatorsOrig)) - - // sanity check - assertInvariants(t, "no operation", - poolOrig, validatorsOrig, - poolOrig, validatorsOrig, 0) - - for j := 0; j < 5; j++ { - poolMod, validatorMod, tokens, msg := randomOperation(r)(r, poolOrig, validatorsOrig[0]) - - validatorsMod := make([]Validator, len(validatorsOrig)) - copy(validatorsMod[:], validatorsOrig[:]) - require.Equal(t, 1, len(validatorsOrig), "j %v", j) - require.Equal(t, 1, len(validatorsMod), "j %v", j) - validatorsMod[0] = validatorMod - - assertInvariants(t, msg, - poolOrig, validatorsOrig, - poolMod, validatorsMod, tokens) - - poolOrig = poolMod - validatorsOrig = validatorsMod - } - } -} - -// run random operations in a random order on a random multi-validator state, assert invariants hold -func TestMultiValidatorIntegrationInvariants(t *testing.T) { - r := rand.New(rand.NewSource(42)) - - for i := 0; i < 10; i++ { - poolOrig, validatorsOrig := randomSetup(r, 100) - - assertInvariants(t, "no operation", - poolOrig, validatorsOrig, - poolOrig, validatorsOrig, 0) - - for j := 0; j < 5; j++ { - index := int(r.Int31n(int32(len(validatorsOrig)))) - poolMod, validatorMod, tokens, msg := randomOperation(r)(r, poolOrig, validatorsOrig[index]) - validatorsMod := make([]Validator, len(validatorsOrig)) - copy(validatorsMod[:], validatorsOrig[:]) - validatorsMod[index] = validatorMod - - assertInvariants(t, msg, - poolOrig, validatorsOrig, - poolMod, validatorsMod, tokens) - - poolOrig = poolMod - validatorsOrig = validatorsMod - - } - } -} diff --git a/x/stake/tick_test.go b/x/stake/tick_test.go index 93efe0088..bc4abca8b 100644 --- a/x/stake/tick_test.go +++ b/x/stake/tick_test.go @@ -68,7 +68,7 @@ func TestProcessProvisions(t *testing.T) { // create some validators some bonded, some unbonded validators := make([]Validator, 10) for i := 0; i < 10; i++ { - c := Validator{ + v := Validator{ Status: sdk.Unbonded, PubKey: pks[i], Address: addrs[i], @@ -76,14 +76,14 @@ func TestProcessProvisions(t *testing.T) { DelegatorShares: sdk.NewRat(0), } if i < 5 { - c.Status = sdk.Bonded + v.Status = sdk.Bonded } mintedTokens := int64((i + 1) * 10000000) pool.TotalSupply += mintedTokens - pool, c, _ = pool.validatorAddTokens(c, mintedTokens) + v, pool, _ = v.addTokens(pool, mintedTokens) - keeper.setValidator(ctx, c) - validators[i] = c + keeper.setValidator(ctx, v) + validators[i] = v } keeper.setPool(ctx, pool) var totalSupply int64 = 550000000 diff --git a/x/stake/validator.go b/x/stake/validator.go index 46c5e9a6e..85befb706 100644 --- a/x/stake/validator.go +++ b/x/stake/validator.go @@ -109,13 +109,16 @@ func NewDescription(moniker, identity, website, details string) Description { } } +//XXX updateDescription function +//XXX enforce limit to number of description characters + // get the exchange rate of tokens over delegator shares // UNITS: eq-val-bonded-shares/delegator-shares func (v Validator) DelegatorShareExRate(p Pool) sdk.Rat { if v.DelegatorShares.IsZero() { return sdk.OneRat() } - tokens := p.EquivalentBondedShares(v) + tokens := v.EquivalentBondedShares(p) return tokens.Quo(v.DelegatorShares) } @@ -136,8 +139,116 @@ func (v Validator) abciValidatorZero(cdc *wire.Codec) abci.Validator { } } -//XXX updateDescription function -//XXX enforce limit to number of description characters +// XXX write test +// update the location of the shares within a validator if its bond status has changed +func (v Validator) UpdateSharesLocation(p Pool) (Validator, Pool) { + var tokens int64 + + switch { + case !v.BondedShares.IsZero(): + if v.Status == sdk.Bonded { // return if nothing needs switching + return v, p + } + p, tokens = p.removeSharesBonded(v.BondedShares) + case !v.UnbondingShares.IsZero(): + if v.Status == sdk.Unbonding { + return v, p + } + p, tokens = p.removeSharesUnbonding(v.BondedShares) + case !v.UnbondedShares.IsZero(): + if v.Status == sdk.Unbonding { + return v, p + } + p, tokens = p.removeSharesUnbonded(v.BondedShares) + } + + switch v.Status { + case sdk.Bonded: + p, v.BondedShares = p.addTokensBonded(tokens) + case sdk.Unbonding: + p, v.UnbondingShares = p.addTokensUnbonding(tokens) + case sdk.Unbonded, sdk.Revoked: + p, v.UnbondedShares = p.addTokensUnbonded(tokens) + } + + return v, p +} + +// XXX TEST +// get the power or potential power for a validator +// if bonded, the power is the BondedShares +// if not bonded, the power is the amount of bonded shares which the +// the validator would have it was bonded +func (v Validator) EquivalentBondedShares(p Pool) (power sdk.Rat) { + switch v.Status { + case sdk.Bonded: + power = v.BondedShares + case sdk.Unbonding: + shares := v.UnbondingShares // ubShr + exRate := p.unbondingShareExRate().Quo(p.bondedShareExRate()) // (tok/ubshr)/(tok/bshr) = bshr/ubshr + power = shares.Mul(exRate) // ubshr*bshr/ubshr = bshr + case sdk.Unbonded, sdk.Revoked: + shares := v.UnbondedShares // ubShr + exRate := p.unbondedShareExRate().Quo(p.bondedShareExRate()) // (tok/ubshr)/(tok/bshr) = bshr/ubshr + power = shares.Mul(exRate) // ubshr*bshr/ubshr = bshr + } + return +} + +// TODO Implement Use in query functionality +// get the equivalent amount of tokens contained by a validator +func (v Validator) Tokens(p Pool) sdk.Rat { + switch v.Status { + case sdk.Bonded: + return p.unbondedShareExRate().Mul(v.BondedShares) // (tokens/shares) * shares + case sdk.Unbonding: + return p.unbondedShareExRate().Mul(v.UnbondingShares) + case sdk.Unbonded, sdk.Revoked: + return p.unbondedShareExRate().Mul(v.UnbondedShares) + } + return sdk.ZeroRat() +} + +// XXX Audit this function further to make sure it's correct +// add tokens to a validator +func (v Validator) addTokens(p Pool, + amount int64) (validator2 Validator, p2 Pool, issuedDelegatorShares sdk.Rat) { + + var poolShares sdk.Rat + switch v.Status { + case sdk.Bonded: + p, poolShares = p.addTokensBonded(amount) + v.BondedShares = v.BondedShares.Add(poolShares) + case sdk.Unbonding: + p, poolShares = p.addTokensUnbonding(amount) + v.UnbondingShares = v.UnbondingShares.Add(poolShares) + case sdk.Unbonded, sdk.Revoked: + p, poolShares = p.addTokensUnbonded(amount) + v.UnbondedShares = v.UnbondedShares.Add(poolShares) + } + + equivalentBondedShares := v.EquivalentBondedShares(p) + exRate := v.DelegatorShareExRate(p) // eq-val-bonded-shares/delegator-shares + issuedDelegatorShares = equivalentBondedShares.Quo(exRate) + v.DelegatorShares = v.DelegatorShares.Add(issuedDelegatorShares) + + return v, p, issuedDelegatorShares +} + +// remove shares from a validator +func (v Validator) removeShares(p Pool, + delShares sdk.Rat) (validator2 Validator, p2 Pool, createdCoins int64) { + + globalPoolSharesToRemove := v.DelegatorShareExRate(p).Mul(delShares) + if v.Status == sdk.Bonded { + p, createdCoins = p.removeSharesBonded(globalPoolSharesToRemove) + } else { + p, createdCoins = p.removeSharesUnbonded(globalPoolSharesToRemove) + } + v.BondedShares = v.BondedShares.Sub(globalPoolSharesToRemove) + v.DelegatorShares = v.DelegatorShares.Sub(delShares) + return v, p, createdCoins +} //______________________________________________________________________ diff --git a/x/stake/validator_test.go b/x/stake/validator_test.go new file mode 100644 index 000000000..3202c31e0 --- /dev/null +++ b/x/stake/validator_test.go @@ -0,0 +1,420 @@ +package stake + +import ( + "fmt" + "math/rand" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAddTokens(t *testing.T) { + ctx, _, keeper := createTestInput(t, false, 0) + + poolA := keeper.GetPool(ctx) + valA := Validator{ + Status: sdk.Bonded, + Address: addrs[0], + PubKey: pks[0], + BondedShares: sdk.NewRat(9), + DelegatorShares: sdk.NewRat(9), + } + poolA.BondedPool = valA.BondedShares.Evaluate() + poolA.BondedShares = valA.BondedShares + assert.Equal(t, valA.DelegatorShareExRate(), sdk.OneRat()) + assert.Equal(t, poolA.bondedShareExRate(), sdk.OneRat()) + assert.Equal(t, poolA.unbondedShareExRate(), sdk.OneRat()) + valB, poolB, sharesB := valA.addTokens(poolA, 10) + + // shares were issued + assert.Equal(t, sdk.NewRat(10).Mul(valA.DelegatorShareExRate()), sharesB) + // pool shares were added + assert.Equal(t, valB.BondedShares, valA.BondedShares.Add(sdk.NewRat(10))) + // conservation of tokens + assert.Equal(t, poolB.BondedPool, 10+poolA.BondedPool) +} + +func TestRemoveShares(t *testing.T) { + ctx, _, keeper := createTestInput(t, false, 0) + + poolA := keeper.GetPool(ctx) + valA := Validator{ + Status: sdk.Bonded, + Address: addrs[0], + PubKey: pks[0], + BondedShares: sdk.NewRat(9), + DelegatorShares: sdk.NewRat(9), + } + poolA.BondedPool = valA.BondedShares.Evaluate() + poolA.BondedShares = valA.BondedShares + assert.Equal(t, valA.DelegatorShareExRate(), sdk.OneRat()) + assert.Equal(t, poolA.bondedShareExRate(), sdk.OneRat()) + assert.Equal(t, poolA.unbondedShareExRate(), sdk.OneRat()) + valB, poolB, coinsB := valA.removeShares(poolA, sdk.NewRat(10)) + + // coins were created + assert.Equal(t, coinsB, int64(10)) + // pool shares were removed + assert.Equal(t, valB.BondedShares, valA.BondedShares.Sub(sdk.NewRat(10).Mul(valA.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) + val := Validator{ + Status: sdk.Bonded, + Address: addrs[0], + PubKey: pks[0], + BondedShares: assets, + DelegatorShares: 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("validator %s (status: %d, assets: %v, liabilities: %v, DelegatorShareExRate: %v)", + val.Address, val.Status, val.BondedShares, val.DelegatorShares, val.DelegatorShareExRate()) + msg = fmt.Sprintf("Removed %v shares from %s", shares, msg) + _, newPool, tokens := val.removeShares(pool, shares) + require.Equal(t, + tokens+newPool.UnbondedPool+newPool.BondedPool, + pool.BondedPool+pool.UnbondedPool, + "Tokens were not conserved: %s", msg) +} + +// TODO convert these commend out tests to test UpdateSharesLocation +//func TestUpdateSharesLocation(t *testing.T) { +//} +//func TestBondedToUnbondedPool(t *testing.T) { +//ctx, _, keeper := createTestInput(t, false, 0) + +//poolA := keeper.GetPool(ctx) +//assert.Equal(t, poolA.bondedShareExRate(), sdk.OneRat()) +//assert.Equal(t, poolA.unbondedShareExRate(), sdk.OneRat()) +//valA := Validator{ +//Status: sdk.Bonded, +//Address: addrs[0], +//PubKey: pks[0], +//BondedShares: sdk.OneRat(), +//DelegatorShares: sdk.OneRat(), +//} +//poolB, valB := poolA.bondedToUnbondedPool(valA) + +//// status unbonded +//assert.Equal(t, valB.Status, sdk.Unbonded) +//// same exchange rate, assets unchanged +//assert.Equal(t, valB.BondedShares, valA.BondedShares) +//// bonded pool decreased +//assert.Equal(t, poolB.BondedPool, poolA.BondedPool-valA.BondedShares.Evaluate()) +//// unbonded pool increased +//assert.Equal(t, poolB.UnbondedPool, poolA.UnbondedPool+valA.BondedShares.Evaluate()) +//// conservation of tokens +//assert.Equal(t, poolB.UnbondedPool+poolB.BondedPool, poolA.BondedPool+poolA.UnbondedPool) +//} + +//func TestUnbonbedtoBondedPool(t *testing.T) { +//ctx, _, keeper := createTestInput(t, false, 0) + +//poolA := keeper.GetPool(ctx) +//assert.Equal(t, poolA.bondedShareExRate(), sdk.OneRat()) +//assert.Equal(t, poolA.unbondedShareExRate(), sdk.OneRat()) +//valA := Validator{ +//Status: sdk.Bonded, +//Address: addrs[0], +//PubKey: pks[0], +//BondedShares: sdk.OneRat(), +//DelegatorShares: sdk.OneRat(), +//} +//valA.Status = sdk.Unbonded +//poolB, valB := poolA.unbondedToBondedPool(valA) + +//// status bonded +//assert.Equal(t, valB.Status, sdk.Bonded) +//// same exchange rate, assets unchanged +//assert.Equal(t, valB.BondedShares, valA.BondedShares) +//// bonded pool increased +//assert.Equal(t, poolB.BondedPool, poolA.BondedPool+valA.BondedShares.Evaluate()) +//// unbonded pool decreased +//assert.Equal(t, poolB.UnbondedPool, poolA.UnbondedPool-valA.BondedShares.Evaluate()) +//// conservation of tokens +//assert.Equal(t, poolB.UnbondedPool+poolB.BondedPool, poolA.BondedPool+poolA.UnbondedPool) +//} + +//________________________________________________________________________________ +// TODO refactor this random setup + +// generate a random validator +func randomValidator(r *rand.Rand) Validator { + var status sdk.BondStatus + if r.Float64() < float64(0.5) { + status = sdk.Bonded + } else { + status = sdk.Unbonded + } + assets := sdk.NewRat(int64(r.Int31n(10000))) + liabilities := sdk.NewRat(int64(r.Int31n(10000))) + return Validator{ + Status: status, + Address: addrs[0], + PubKey: pks[0], + BondedShares: assets, + DelegatorShares: liabilities, + } +} + +// generate a random staking state +func randomSetup(r *rand.Rand, numValidators int) (Pool, Validators) { + pool := Pool{ + TotalSupply: 0, + BondedShares: sdk.ZeroRat(), + UnbondedShares: sdk.ZeroRat(), + BondedPool: 0, + UnbondedPool: 0, + InflationLastTime: 0, + Inflation: sdk.NewRat(7, 100), + } + + validators := make([]Validator, numValidators) + for i := 0; i < numValidators; i++ { + validator := randomValidator(r) + if validator.Status == sdk.Bonded { + pool.BondedShares = pool.BondedShares.Add(validator.BondedShares) + pool.BondedPool += validator.BondedShares.Evaluate() + } else if validator.Status == sdk.Unbonded { + pool.UnbondedShares = pool.UnbondedShares.Add(validator.BondedShares) + pool.UnbondedPool += validator.BondedShares.Evaluate() + } + validators[i] = validator + } + return pool, validators +} + +// any operation that transforms staking state +// takes in RNG instance, pool, validator +// returns updated pool, updated validator, delta tokens, descriptive message +type Operation func(r *rand.Rand, p Pool, c Validator) (Pool, Validator, int64, string) + +// operation: bond or unbond a validator depending on current status +func OpBondOrUnbond(r *rand.Rand, p Pool, val Validator) (Pool, Validator, int64, string) { + var msg string + if val.Status == sdk.Bonded { + msg = fmt.Sprintf("sdk.Unbonded previously bonded validator %s (assets: %v, liabilities: %v, DelegatorShareExRate: %v)", + val.Address, val.BondedShares, val.DelegatorShares, val.DelegatorShareExRate()) + val.Status = sdk.Unbonded + } else if val.Status == sdk.Unbonded { + msg = fmt.Sprintf("sdk.Bonded previously unbonded validator %s (assets: %v, liabilities: %v, DelegatorShareExRate: %v)", + val.Address, val.BondedShares, val.DelegatorShares, val.DelegatorShareExRate()) + val.Status = sdk.Bonded + } + val, p = val.UpdateSharesLocation(p) + return p, val, 0, msg +} + +// operation: add a random number of tokens to a validator +func OpAddTokens(r *rand.Rand, p Pool, val Validator) (Pool, Validator, int64, string) { + tokens := int64(r.Int31n(1000)) + msg := fmt.Sprintf("validator %s (status: %d, assets: %v, liabilities: %v, DelegatorShareExRate: %v)", + val.Address, val.Status, val.BondedShares, val.DelegatorShares, val.DelegatorShareExRate()) + val, p, _ = val.addTokens(p, tokens) + msg = fmt.Sprintf("Added %d tokens to %s", tokens, msg) + return p, val, -1 * tokens, msg // tokens are removed so for accounting must be negative +} + +// operation: remove a random number of shares from a validator +func OpRemoveShares(r *rand.Rand, p Pool, val Validator) (Pool, Validator, int64, string) { + var shares sdk.Rat + for { + shares = sdk.NewRat(int64(r.Int31n(1000))) + if shares.LT(val.DelegatorShares) { + break + } + } + + msg := fmt.Sprintf("Removed %v shares from validator %s (status: %d, assets: %v, liabilities: %v, DelegatorShareExRate: %v)", + shares, val.Address, val.Status, val.BondedShares, val.DelegatorShares, val.DelegatorShareExRate()) + + val, p, tokens := val.removeShares(p, shares) + return p, val, tokens, msg +} + +// pick a random staking operation +func randomOperation(r *rand.Rand) Operation { + operations := []Operation{ + OpBondOrUnbond, + OpAddTokens, + OpRemoveShares, + } + 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, msg string, + pOrig Pool, cOrig Validators, pMod Pool, cMods Validators, tokens int64) { + + // total tokens conserved + require.Equal(t, + pOrig.UnbondedPool+pOrig.BondedPool, + pMod.UnbondedPool+pMod.BondedPool+tokens, + "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 bonded shares + require.False(t, pMod.BondedShares.LT(sdk.ZeroRat()), + "Negative bonded shares - msg: %v\npOrig: %#v\npMod: %#v\ntokens: %v\n", + msg, pOrig, pMod, tokens) + + // 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()) + + for _, cMod := range cMods { + + // nonnegative ex rate + require.False(t, cMod.DelegatorShareExRate().LT(sdk.ZeroRat()), + "Applying operation \"%s\" resulted in negative validator.DelegatorShareExRate(): %v (validator.Address: %s)", + msg, + cMod.DelegatorShareExRate(), + cMod.Address, + ) + + // nonnegative assets + require.False(t, cMod.BondedShares.LT(sdk.ZeroRat()), + "Applying operation \"%s\" resulted in negative validator.BondedShares: %v (validator.DelegatorShares: %v, validator.DelegatorShareExRate: %v, validator.Address: %s)", + msg, + cMod.BondedShares, + cMod.DelegatorShares, + cMod.DelegatorShareExRate(), + cMod.Address, + ) + + // nonnegative liabilities + require.False(t, cMod.DelegatorShares.LT(sdk.ZeroRat()), + "Applying operation \"%s\" resulted in negative validator.DelegatorShares: %v (validator.BondedShares: %v, validator.DelegatorShareExRate: %v, validator.Address: %s)", + msg, + cMod.DelegatorShares, + cMod.BondedShares, + cMod.DelegatorShareExRate(), + cMod.Address, + ) + + } + +} + +func TestPossibleOverflow(t *testing.T) { + assets := sdk.NewRat(2159) + liabilities := sdk.NewRat(391432570689183511).Quo(sdk.NewRat(40113011844664)) + val := Validator{ + Status: sdk.Bonded, + Address: addrs[0], + PubKey: pks[0], + BondedShares: assets, + DelegatorShares: 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("validator %s (status: %d, assets: %v, liabilities: %v, DelegatorShareExRate: %v)", + val.Address, val.Status, val.BondedShares, val.DelegatorShares, val.DelegatorShareExRate()) + newValidator, _, _ := val.addTokens(pool, tokens) + + msg = fmt.Sprintf("Added %d tokens to %s", tokens, msg) + require.False(t, newValidator.DelegatorShareExRate().LT(sdk.ZeroRat()), + "Applying operation \"%s\" resulted in negative DelegatorShareExRate(): %v", + msg, newValidator.DelegatorShareExRate()) +} + +// run random operations in a random order on a random single-validator state, assert invariants hold +func TestSingleValidatorIntegrationInvariants(t *testing.T) { + r := rand.New(rand.NewSource(41)) + + for i := 0; i < 10; i++ { + poolOrig, validatorsOrig := randomSetup(r, 1) + require.Equal(t, 1, len(validatorsOrig)) + + // sanity check + assertInvariants(t, "no operation", + poolOrig, validatorsOrig, + poolOrig, validatorsOrig, 0) + + for j := 0; j < 5; j++ { + poolMod, validatorMod, tokens, msg := randomOperation(r)(r, poolOrig, validatorsOrig[0]) + + validatorsMod := make([]Validator, len(validatorsOrig)) + copy(validatorsMod[:], validatorsOrig[:]) + require.Equal(t, 1, len(validatorsOrig), "j %v", j) + require.Equal(t, 1, len(validatorsMod), "j %v", j) + validatorsMod[0] = validatorMod + + assertInvariants(t, msg, + poolOrig, validatorsOrig, + poolMod, validatorsMod, tokens) + + poolOrig = poolMod + validatorsOrig = validatorsMod + } + } +} + +// run random operations in a random order on a random multi-validator state, assert invariants hold +func TestMultiValidatorIntegrationInvariants(t *testing.T) { + r := rand.New(rand.NewSource(42)) + + for i := 0; i < 10; i++ { + poolOrig, validatorsOrig := randomSetup(r, 100) + + assertInvariants(t, "no operation", + poolOrig, validatorsOrig, + poolOrig, validatorsOrig, 0) + + for j := 0; j < 5; j++ { + index := int(r.Int31n(int32(len(validatorsOrig)))) + poolMod, validatorMod, tokens, msg := randomOperation(r)(r, poolOrig, validatorsOrig[index]) + validatorsMod := make([]Validator, len(validatorsOrig)) + copy(validatorsMod[:], validatorsOrig[:]) + validatorsMod[index] = validatorMod + + assertInvariants(t, msg, + poolOrig, validatorsOrig, + poolMod, validatorsMod, tokens) + + poolOrig = poolMod + validatorsOrig = validatorsMod + + } + } +}