From 45e40fe35781a299f5689062b275fbfdd8d32672 Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Tue, 14 Apr 2020 14:42:08 -0700 Subject: [PATCH] [WIP] Kavadist Simulations (#435) * feat: kavadist sims * refactor genesis, add validation * implement params * rename simulation to genesis Co-authored-by: Kevin Davis --- x/kavadist/simulation/genesis.go | 84 +++++++++++++++++++++++++++++ x/kavadist/simulation/params.go | 24 ++++++++- x/kavadist/simulation/simulation.go | 22 -------- x/kavadist/simulation/utils.go | 64 ++++++++++++++++++++++ x/kavadist/simulation/utils_test.go | 30 +++++++++++ x/kavadist/types/params.go | 9 ++++ 6 files changed, 209 insertions(+), 24 deletions(-) create mode 100644 x/kavadist/simulation/genesis.go delete mode 100644 x/kavadist/simulation/simulation.go create mode 100644 x/kavadist/simulation/utils.go create mode 100644 x/kavadist/simulation/utils_test.go diff --git a/x/kavadist/simulation/genesis.go b/x/kavadist/simulation/genesis.go new file mode 100644 index 00000000..d12a73d5 --- /dev/null +++ b/x/kavadist/simulation/genesis.go @@ -0,0 +1,84 @@ +package simulation + +import ( + "fmt" + "math/rand" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/kava-labs/kava/x/kavadist/types" +) + +// SecondsPerYear is the number of seconds in a year +const SecondsPerYear = 31536000 + +// BaseAprPadding prevents the calculated SPR inflation rate from being 0.0 +const BaseAprPadding = "0.000000000100000000" + +// RandomizedGenState generates a random GenesisState for kavadist module +func RandomizedGenState(simState *module.SimulationState) { + params := genRandomParams(simState) + if err := params.Validate(); err != nil { + panic(err) + } + + kavadistGenesis := types.NewGenesisState(params, types.DefaultPreviousBlockTime) + if err := kavadistGenesis.Validate(); err != nil { + panic(err) + } + + fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, kavadistGenesis)) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(kavadistGenesis) +} + +func genRandomParams(simState *module.SimulationState) types.Params { + periods := genRandomPeriods(simState.Rand, simState.GenTimestamp) + params := types.NewParams(true, periods) + return params +} + +func genRandomPeriods(r *rand.Rand, timestamp time.Time) types.Periods { + var periods types.Periods + numPeriods := simulation.RandIntBetween(r, 1, 10) + periodStart := timestamp + for i := 0; i < numPeriods; i++ { + // set periods to be between 2 weeks and 2 years + durationMultiplier := simulation.RandIntBetween(r, 14, 104) + duration := time.Duration(int64(24*durationMultiplier)) * time.Hour + periodEnd := periodStart.Add(duration) + inflation := genRandomInflation(r) + period := types.NewPeriod(periodStart, periodEnd, inflation) + periods = append(periods, period) + periodStart = periodEnd + } + return periods +} + +func genRandomInflation(r *rand.Rand) sdk.Dec { + // If sim.RandomDecAmount returns 0 (happens frequently by design), add BaseAprPadding + extraAprInflation := simulation.RandomDecAmount(r, sdk.MustNewDecFromStr("0.25")) + for extraAprInflation.Equal(sdk.ZeroDec()) { + extraAprInflation = extraAprInflation.Add(sdk.MustNewDecFromStr(BaseAprPadding)) + } + + aprInflation := sdk.OneDec().Add(extraAprInflation) + // convert APR inflation to SPR (inflation per second) + inflationSpr, err := approxRoot(aprInflation, uint64(SecondsPerYear)) + if err != nil { + panic(fmt.Sprintf("error generating random inflation %v", err)) + } + return inflationSpr +} + +func genRandomActive(r *rand.Rand) bool { + threshold := 50 + value := simulation.RandIntBetween(r, 1, 100) + if value > threshold { + return true + } + return false +} diff --git a/x/kavadist/simulation/params.go b/x/kavadist/simulation/params.go index 8c1f7aff..909e3baf 100644 --- a/x/kavadist/simulation/params.go +++ b/x/kavadist/simulation/params.go @@ -1,14 +1,34 @@ package simulation import ( + "fmt" "math/rand" "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/kava-labs/kava/x/kavadist/types" ) // ParamChanges defines the parameters that can be modified by param change proposals // on the simulation func ParamChanges(r *rand.Rand) []simulation.ParamChange { - // TODO implement this - return []simulation.ParamChange{} + // Hacky way to validate periods since validation is wrapped in params + active := genRandomActive(r) + periods := genRandomPeriods(r, simulation.RandTimestamp(r)) + if err := types.NewParams(active, periods).Validate(); err != nil { + panic(err) + } + + return []simulation.ParamChange{ + simulation.NewSimParamChange(types.ModuleName, string(types.KeyActive), "", + func(r *rand.Rand) string { + return fmt.Sprintf("\"%t\"", active) + }, + ), + simulation.NewSimParamChange(types.ModuleName, string(types.KeyPeriods), "", + func(r *rand.Rand) string { + return fmt.Sprintf("\"%v\"", periods) + }, + ), + } } diff --git a/x/kavadist/simulation/simulation.go b/x/kavadist/simulation/simulation.go deleted file mode 100644 index a1fbd964..00000000 --- a/x/kavadist/simulation/simulation.go +++ /dev/null @@ -1,22 +0,0 @@ -package simulation - -import ( - "fmt" - - "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/types/module" - - "github.com/kava-labs/kava/x/kavadist/types" -) - -// RandomizedGenState generates a random GenesisState for cdp -func RandomizedGenState(simState *module.SimulationState) { - - // TODO implement this fully - // - randomly generating the genesis params - // - overwriting with genesis provided to simulation - genesis := types.DefaultGenesisState() - - fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, genesis)) - simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesis) -} diff --git a/x/kavadist/simulation/utils.go b/x/kavadist/simulation/utils.go new file mode 100644 index 00000000..b6f771b7 --- /dev/null +++ b/x/kavadist/simulation/utils.go @@ -0,0 +1,64 @@ +package simulation + +import ( + "errors" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// TODO use the official version available in v0.38.2 of the sdk +// https://github.com/cosmos/cosmos-sdk/blob/e8d89a2fe26175b73545a3e79ae783032b4e975e/types/decimal.go#L328 +func approxRoot(d sdk.Dec, root uint64) (guess sdk.Dec, err error) { + defer func() { + if r := recover(); r != nil { + var ok bool + err, ok = r.(error) + if !ok { + err = errors.New("out of bounds") + } + } + }() + + if d.IsNegative() { + absRoot, err := approxRoot(d.MulInt64(-1), root) + return absRoot.MulInt64(-1), err + } + if root == 1 || d.IsZero() || d.Equal(sdk.OneDec()) { + return d, nil + } + if root == 0 { + return sdk.OneDec(), nil + } + rootInt := sdk.NewInt(int64(root)) + guess, delta := sdk.OneDec(), sdk.OneDec() + for delta.Abs().GT(sdk.SmallestDec()) { + prev := power(guess, (root - 1)) + if prev.IsZero() { + prev = sdk.SmallestDec() + } + delta = d.Quo(prev) + delta = delta.Sub(guess) + delta = delta.QuoInt(rootInt) + + guess = guess.Add(delta) + } + return guess, nil +} + +// Power returns a the result of raising to a positive integer power +func power(d sdk.Dec, power uint64) sdk.Dec { + if power == 0 { + return sdk.OneDec() + } + tmp := sdk.OneDec() + for i := power; i > 1; { + if i%2 == 0 { + i /= 2 + } else { + tmp = tmp.Mul(d) + i = (i - 1) / 2 + } + d = d.Mul(d) + } + return d.Mul(tmp) +} diff --git a/x/kavadist/simulation/utils_test.go b/x/kavadist/simulation/utils_test.go new file mode 100644 index 00000000..81a76c39 --- /dev/null +++ b/x/kavadist/simulation/utils_test.go @@ -0,0 +1,30 @@ +package simulation + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestApproxRoot(t *testing.T) { + testCases := []struct { + input sdk.Dec + root uint64 + expected sdk.Dec + }{ + {sdk.OneDec(), 10, sdk.OneDec()}, // 1.0 ^ (0.1) => 1.0 + {sdk.NewDecWithPrec(25, 2), 2, sdk.NewDecWithPrec(5, 1)}, // 0.25 ^ (0.5) => 0.5 + {sdk.NewDecWithPrec(4, 2), 2, sdk.NewDecWithPrec(2, 1)}, // 0.04 => 0.2 + {sdk.NewDecFromInt(sdk.NewInt(27)), 3, sdk.NewDecFromInt(sdk.NewInt(3))}, // 27 ^ (1/3) => 3 + {sdk.NewDecFromInt(sdk.NewInt(-81)), 4, sdk.NewDecFromInt(sdk.NewInt(-3))}, // -81 ^ (0.25) => -3 + {sdk.NewDecFromInt(sdk.NewInt(2)), 2, sdk.NewDecWithPrec(1414213562373095049, 18)}, // 2 ^ (0.5) => 1.414213562373095049 + {sdk.NewDecWithPrec(1005, 3), 31536000, sdk.MustNewDecFromStr("1.000000000158153904")}, // 1.005 ^ (1/31536000) = 1.000000000158153904 + } + + for i, tc := range testCases { + res, err := approxRoot(tc.input, tc.root) + require.NoError(t, err) + require.True(t, tc.expected.Sub(res).Abs().LTE(sdk.SmallestDec()), "unexpected result for test case %d, input: %v", i, tc.input) + } +} diff --git a/x/kavadist/types/params.go b/x/kavadist/types/params.go index 6cb955b0..d2ab89e0 100644 --- a/x/kavadist/types/params.go +++ b/x/kavadist/types/params.go @@ -33,6 +33,15 @@ type Period struct { Inflation sdk.Dec `json:"inflation" yaml:"inflation"` // example "1.000000003022265980" - 10% inflation } +// NewPeriod returns a new instance of Period +func NewPeriod(start time.Time, end time.Time, inflation sdk.Dec) Period { + return Period{ + Start: start, + End: end, + Inflation: inflation, + } +} + // String implements fmt.Stringer func (pr Period) String() string { return fmt.Sprintf(`Period: