From 14877e9ef3b3f404e17f8a85df2bbda22d3ad4d6 Mon Sep 17 00:00:00 2001 From: keruch <53012408+keruch@users.noreply.github.com> Date: Fri, 20 Sep 2024 12:54:24 +0200 Subject: [PATCH] feat(streamer): distribute rewards immediately in the current block (#1173) --- app/keepers/keepers.go | 2 +- .../dymension/streamer/stream.proto | 2 +- utils/cache/ordered.go | 93 + utils/cache/ordered_test.go | 163 ++ utils/pagination/paginate.go | 35 +- utils/pagination/paginate_test.go | 150 +- x/incentives/keeper/distribute.go | 50 +- x/incentives/keeper/distribute_test.go | 13 +- x/incentives/keeper/gauge.go | 10 - x/incentives/keeper/gauge_asset.go | 15 +- x/incentives/keeper/gauge_rollapp.go | 5 - x/incentives/keeper/gauge_rollapp_test.go | 6 +- x/incentives/keeper/gauge_test.go | 6 +- x/incentives/keeper/grpc_query_test.go | 8 +- x/incentives/keeper/hooks.go | 2 +- x/incentives/types/types.go | 23 + x/streamer/keeper/abci.go | 42 +- x/streamer/keeper/abci_test.go | 1552 ++++++++++++++--- x/streamer/keeper/distr_info.go | 10 +- x/streamer/keeper/distr_info_test.go | 5 +- x/streamer/keeper/distribute.go | 260 ++- x/streamer/keeper/distribute_test.go | 15 +- x/streamer/keeper/distributed_coins.go | 1 + x/streamer/keeper/genesis_test.go | 8 +- x/streamer/keeper/grpc_query.go | 5 +- x/streamer/keeper/grpc_query_test.go | 3 +- x/streamer/keeper/hooks.go | 47 +- x/streamer/keeper/hooks_test.go | 8 +- x/streamer/keeper/invariants.go | 1 + x/streamer/keeper/iterator.go | 4 +- x/streamer/keeper/keeper.go | 11 +- ...keeper_replace_update_distribution_test.go | 1 + x/streamer/keeper/params.go | 4 +- x/streamer/keeper/stream_iterator.go | 2 +- x/streamer/keeper/stream_iterator_test.go | 7 +- x/streamer/keeper/suite_test.go | 37 +- x/streamer/keeper/utils.go | 4 +- x/streamer/keeper/utils_test.go | 3 +- x/streamer/types/distr_info.go | 12 +- x/streamer/types/distr_info_test.go | 12 +- x/streamer/types/expected_keepers.go | 6 +- x/streamer/types/stream.go | 10 +- x/streamer/types/stream.pb.go | 99 +- 43 files changed, 2146 insertions(+), 606 deletions(-) create mode 100644 utils/cache/ordered.go create mode 100644 utils/cache/ordered_test.go create mode 100644 x/incentives/types/types.go diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index e87e70d0e..6f4e3f58f 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -565,8 +565,8 @@ func (a *AppKeepers) SetupHooks() { a.EpochsKeeper.SetHooks( epochstypes.NewMultiEpochHooks( // insert epochs hooks receivers here + a.StreamerKeeper.Hooks(), // x/streamer must be before x/incentives a.IncentivesKeeper.Hooks(), - a.StreamerKeeper.Hooks(), a.TxFeesKeeper.Hooks(), a.DelayedAckKeeper.GetEpochHooks(), a.DymNSKeeper.GetEpochHooks(), diff --git a/proto/dymensionxyz/dymension/streamer/stream.proto b/proto/dymensionxyz/dymension/streamer/stream.proto index 10811461f..957896b7b 100644 --- a/proto/dymensionxyz/dymension/streamer/stream.proto +++ b/proto/dymensionxyz/dymension/streamer/stream.proto @@ -19,7 +19,7 @@ message Stream { uint64 id = 1; // distribute_to is the distr_info. - DistrInfo distribute_to = 2; + DistrInfo distribute_to = 2 [ (gogoproto.nullable) = false ]; // coins is the total amount of coins that have been in the stream // Can distribute multiple coin denoms diff --git a/utils/cache/ordered.go b/utils/cache/ordered.go new file mode 100644 index 000000000..df4d2f1e5 --- /dev/null +++ b/utils/cache/ordered.go @@ -0,0 +1,93 @@ +package cache + +import "fmt" + +// InsertionOrdered is a cache that preserves the insertion order of its elements. +// It maps keys of type K to values of type V and ensures that elements are iterated +// in the order they were inserted. This implementation is NOT thread-safe. +type InsertionOrdered[K comparable, V any] struct { + key func(V) K // A function that generates the key from a value + nextIdx int // The index to assign to the next inserted element + keyToIdx map[K]int // Maps keys to their index in the idxToValue slice + idxToValue []V // Stores values in the order they were inserted +} + +// NewInsertionOrdered creates and returns a new InsertionOrdered cache. +// It accepts a key function, which extracts a key of type K from a value of type V. +// Optionally, you can pass initial values to be inserted into the cache. +func NewInsertionOrdered[K comparable, V any](key func(V) K, initial ...V) *InsertionOrdered[K, V] { + cache := &InsertionOrdered[K, V]{ + key: key, + nextIdx: 0, + keyToIdx: make(map[K]int, len(initial)), + idxToValue: make([]V, 0, len(initial)), + } + // Insert the initial values (if any) into the cache + cache.Upsert(initial...) + return cache +} + +// Upsert inserts or updates one or more values in the cache. +// If a value with the same key already exists, it will be updated. +// If the key is new, the value will be appended while preserving the insertion order. +func (c *InsertionOrdered[K, V]) Upsert(values ...V) { + for _, value := range values { + c.upsert(value) + } +} + +// upsert is an internal helper method that inserts or updates a single value. +// It extracts the key from the value, checks if it already exists in the cache, +// and updates the value if found. If not, it appends the new value to the cache. +func (c *InsertionOrdered[K, V]) upsert(value V) { + key := c.key(value) + idx, ok := c.keyToIdx[key] + if ok { + // If the key already exists, update the value + c.idxToValue[idx] = value + } else { + // If the key does not exist, add a new entry + idx = c.nextIdx + c.nextIdx++ + c.keyToIdx[key] = idx + c.idxToValue = append(c.idxToValue, value) + } +} + +// Get retrieves a value from the cache by its key. +// It returns the value and a boolean indicating whether the key was found. +// If the key does not exist, it returns the zero value of type V and false. +func (c *InsertionOrdered[K, V]) Get(key K) (zero V, found bool) { + idx, ok := c.keyToIdx[key] + if ok { + return c.idxToValue[idx], true + } + return zero, false +} + +// MustGet is Get that panics when the key is not found. +func (c *InsertionOrdered[K, V]) MustGet(key K) V { + value, ok := c.Get(key) + if ok { + return value + } + panic(fmt.Errorf("internal contract error: key is not found in the cache: %v", key)) +} + +// GetAll returns all values currently stored in the cache in their insertion order. +// This allows you to retrieve all values while preserving the order in which they were added. +func (c *InsertionOrdered[K, V]) GetAll() []V { + return c.idxToValue +} + +// Range iterates over the values in the cache in their insertion order. +// The provided function f is called for each value. If f returns true, the iteration stops early. +// This method allows for efficient traversal without needing to copy the entire cache. +func (c *InsertionOrdered[K, V]) Range(f func(V) bool) { + for _, value := range c.idxToValue { + stop := f(value) + if stop { + return + } + } +} diff --git a/utils/cache/ordered_test.go b/utils/cache/ordered_test.go new file mode 100644 index 000000000..e6cd6d5b2 --- /dev/null +++ b/utils/cache/ordered_test.go @@ -0,0 +1,163 @@ +package cache_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/dymensionxyz/dymension/v3/utils/cache" +) + +// Test struct to use in the cache +type testStruct struct { + ID int + Name string +} + +// Key extraction function for testStruct +func keyFunc(val testStruct) int { + return val.ID +} + +func TestInsertionOrdered_Upsert(t *testing.T) { + testCases := []struct { + name string + initial []testStruct + upserts []testStruct + expectedValues []testStruct + }{ + { + name: "Insert single item", + initial: []testStruct{}, + upserts: []testStruct{{ID: 1, Name: "Item 1"}}, + expectedValues: []testStruct{{ID: 1, Name: "Item 1"}}, + }, + { + name: "Insert multiple items", + initial: []testStruct{}, + upserts: []testStruct{{ID: 1, Name: "Item 1"}, {ID: 2, Name: "Item 2"}}, + expectedValues: []testStruct{{ID: 1, Name: "Item 1"}, {ID: 2, Name: "Item 2"}}, + }, + { + name: "Update existing item", + initial: []testStruct{{ID: 1, Name: "Item 1"}}, + upserts: []testStruct{{ID: 1, Name: "Updated Item 1"}}, + expectedValues: []testStruct{{ID: 1, Name: "Updated Item 1"}}, + }, + { + name: "Insert and update items", + initial: []testStruct{{ID: 1, Name: "Item 1"}}, + upserts: []testStruct{{ID: 2, Name: "Item 2"}, {ID: 1, Name: "Updated Item 1"}, {ID: 0, Name: "Item 0"}}, + expectedValues: []testStruct{{ID: 1, Name: "Updated Item 1"}, {ID: 2, Name: "Item 2"}, {ID: 0, Name: "Item 0"}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := cache.NewInsertionOrdered(keyFunc, tc.initial...) + c.Upsert(tc.upserts...) + + // Validate that the cache contains the expected values in the correct order + require.Equal(t, tc.expectedValues, c.GetAll()) + }) + } +} + +func TestInsertionOrdered_Get(t *testing.T) { + testCases := []struct { + name string + initial []testStruct + getID int + expectedVal testStruct + found bool + }{ + { + name: "Get existing item", + initial: []testStruct{{ID: 1, Name: "Item 1"}}, + getID: 1, + expectedVal: testStruct{ID: 1, Name: "Item 1"}, + found: true, + }, + { + name: "Get non-existing item", + initial: []testStruct{{ID: 1, Name: "Item 1"}}, + getID: 2, + expectedVal: testStruct{}, + found: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := cache.NewInsertionOrdered(keyFunc, tc.initial...) + val, found := c.Get(tc.getID) + + require.Equal(t, tc.found, found) + require.Equal(t, tc.expectedVal, val) + }) + } +} + +func TestInsertionOrdered_GetAll(t *testing.T) { + testCases := []struct { + name string + initial []testStruct + expectedValues []testStruct + }{ + { + name: "Get all from empty cache", + initial: []testStruct{}, + expectedValues: []testStruct{}, + }, + { + name: "Get all from non-empty cache", + initial: []testStruct{{ID: 1, Name: "Item 1"}, {ID: 2, Name: "Item 2"}}, + expectedValues: []testStruct{{ID: 1, Name: "Item 1"}, {ID: 2, Name: "Item 2"}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := cache.NewInsertionOrdered(keyFunc, tc.initial...) + allValues := c.GetAll() + + require.Equal(t, tc.expectedValues, allValues) + }) + } +} + +func TestInsertionOrdered_Range(t *testing.T) { + testCases := []struct { + name string + initial []testStruct + stopID int + expectedValues []testStruct + }{ + { + name: "Range over all values", + initial: []testStruct{{ID: 1, Name: "Item 1"}, {ID: 2, Name: "Item 2"}}, + stopID: -1, + expectedValues: []testStruct{{ID: 1, Name: "Item 1"}, {ID: 2, Name: "Item 2"}}, + }, + { + name: "Stop at specific value", + initial: []testStruct{{ID: 1, Name: "Item 1"}, {ID: 2, Name: "Item 2"}}, + stopID: 1, + expectedValues: []testStruct{{ID: 1, Name: "Item 1"}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := cache.NewInsertionOrdered(keyFunc, tc.initial...) + var collected []testStruct + + c.Range(func(v testStruct) bool { + collected = append(collected, v) + return v.ID == tc.stopID + }) + + require.Equal(t, tc.expectedValues, collected) + }) + } +} diff --git a/utils/pagination/paginate.go b/utils/pagination/paginate.go index 5a2df451b..692172f4d 100644 --- a/utils/pagination/paginate.go +++ b/utils/pagination/paginate.go @@ -6,24 +6,23 @@ type Iterator[T any] interface { Valid() bool } -type Stop bool - -const ( - Break Stop = true - Continue Stop = false -) - // Paginate is a function that paginates over an iterator. The callback is executed for each iteration and if it -// returns true, the pagination stops. The function returns the amount of iterations before stopping. -func Paginate[T any](iter Iterator[T], perPage uint64, cb func(T) Stop) uint64 { - iterations := uint64(0) - for ; iterations < perPage && iter.Valid(); iter.Next() { - iterations++ - - stop := cb(iter.Value()) - if stop { - break - } +// returns true, the pagination stops. The callback also returns the number of operations performed during the call. +// That is, one iteration may be complex and thus return >1 operation num. For example, in case if the called decides +// that the iteration is heavy or time-consuming. Paginate also allows to specify the maximum number of operations +// that may be accumulated during the execution. If this number is exceeded, then Paginate exits. +// The function returns the total number of operations performed before stopping. +func Paginate[T any]( + iter Iterator[T], + maxOperations uint64, + cb func(T) (stop bool, operations uint64), +) uint64 { + totalOperations := uint64(0) + stop := false + for ; !stop && totalOperations < maxOperations && iter.Valid(); iter.Next() { + var operations uint64 + stop, operations = cb(iter.Value()) + totalOperations += operations } - return iterations + return totalOperations } diff --git a/utils/pagination/paginate_test.go b/utils/pagination/paginate_test.go index 6e842c3b2..10f685b99 100644 --- a/utils/pagination/paginate_test.go +++ b/utils/pagination/paginate_test.go @@ -31,53 +31,139 @@ func (t *testIterator) Valid() bool { func TestPaginate(t *testing.T) { testCases := []struct { - name string - iterator pagination.Iterator[int] - perPage uint64 - stopValue int - expected uint64 + name string + iterator pagination.Iterator[int] + maxIterations uint64 + stopValue int + expectedIterations uint64 + iterationWeight uint64 }{ { - name: "Empty iterator", - iterator: newTestIterator([]int{}), - perPage: 5, - stopValue: -1, - expected: 0, + name: "Empty iterator", + iterator: newTestIterator([]int{}), + maxIterations: 5, + stopValue: -1, + expectedIterations: 0, + iterationWeight: 1, }, { - name: "Non-Empty iterator less than perPage", - iterator: newTestIterator([]int{1, 2, 3}), - perPage: 10, - stopValue: -1, - expected: 3, + name: "Non-Empty iterator less than maxIterations", + iterator: newTestIterator([]int{1, 2, 3}), + maxIterations: 10, + stopValue: -1, + expectedIterations: 3, + iterationWeight: 1, }, { - name: "Non-empty iterator greater than perPage", - iterator: newTestIterator([]int{1, 2, 3, 4, 5, 6, 7}), - perPage: 5, - stopValue: -1, - expected: 5, + name: "Non-empty iterator greater than maxIterations", + iterator: newTestIterator([]int{1, 2, 3, 4, 5, 6, 7}), + maxIterations: 5, + stopValue: -1, + expectedIterations: 5, + iterationWeight: 1, }, { - name: "Zero perPage", - iterator: newTestIterator([]int{1, 2, 3, 4, 5, 6, 7}), - perPage: 0, - stopValue: 6, - expected: 0, + name: "Zero maxIterations", + iterator: newTestIterator([]int{1, 2, 3, 4, 5, 6, 7}), + maxIterations: 0, + stopValue: 6, + expectedIterations: 0, + iterationWeight: 1, }, { - name: "Non-Empty iterator with stop condition", - iterator: newTestIterator([]int{1, 2, 3, 4, 5, 6, 7}), - perPage: 10, - stopValue: 3, - expected: 3, + name: "Non-Empty iterator with stop condition", + iterator: newTestIterator([]int{1, 2, 3, 4, 5, 6, 7}), + maxIterations: 10, + stopValue: 3, + expectedIterations: 3, + iterationWeight: 1, + }, + { + name: "Empty iterator, >1 iteration weight", + iterator: newTestIterator([]int{}), + maxIterations: 5, + stopValue: -1, + expectedIterations: 0, + iterationWeight: 3, + }, + { + name: "Non-Empty iterator less than maxIterations, >1 iteration weight", + iterator: newTestIterator([]int{1, 2, 3}), + maxIterations: 10, + stopValue: -1, + expectedIterations: 9, + iterationWeight: 3, + }, + { + name: "Non-empty iterator greater than maxIterations, >1 iteration weight", + iterator: newTestIterator([]int{1, 2, 3, 4, 5, 6, 7}), + maxIterations: 5, + stopValue: -1, + expectedIterations: 6, + iterationWeight: 3, + }, + { + name: "Zero maxIterations, >1 iteration weight", + iterator: newTestIterator([]int{1, 2, 3, 4, 5, 6, 7}), + maxIterations: 0, + stopValue: 6, + expectedIterations: 0, + iterationWeight: 3, + }, + { + name: "Non-Empty iterator with stop condition, >1 iteration weight", + iterator: newTestIterator([]int{1, 2, 3, 4, 5, 6, 7}), + maxIterations: 10, + stopValue: 3, + expectedIterations: 9, + iterationWeight: 3, + }, + { + name: "Empty iterator, 0 iteration weight", + iterator: newTestIterator([]int{}), + maxIterations: 5, + stopValue: -1, + expectedIterations: 0, + iterationWeight: 0, + }, + { + name: "Non-Empty iterator less than maxIterations, 0 iteration weight", + iterator: newTestIterator([]int{1, 2, 3}), + maxIterations: 10, + stopValue: -1, + expectedIterations: 0, + iterationWeight: 0, + }, + { + name: "Non-empty iterator greater than maxIterations, 0 iteration weight", + iterator: newTestIterator([]int{1, 2, 3, 4, 5, 6, 7}), + maxIterations: 5, + stopValue: -1, + expectedIterations: 0, + iterationWeight: 0, + }, + { + name: "Zero maxIterations, 0 iteration weight", + iterator: newTestIterator([]int{1, 2, 3, 4, 5, 6, 7}), + maxIterations: 0, + stopValue: 6, + expectedIterations: 0, + iterationWeight: 0, + }, + { + name: "Non-Empty iterator with stop condition, 0 iteration weight", + iterator: newTestIterator([]int{1, 2, 3, 4, 5, 6, 7}), + maxIterations: 10, + stopValue: 3, + expectedIterations: 0, + iterationWeight: 0, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - result := pagination.Paginate(tc.iterator, tc.perPage, func(i int) pagination.Stop { return i == tc.stopValue }) - require.Equal(t, tc.expected, result) + result := pagination.Paginate(tc.iterator, tc.maxIterations, func(i int) (bool, uint64) { return i == tc.stopValue, tc.iterationWeight }) + require.Equal(t, tc.expectedIterations, result) }) } } diff --git a/x/incentives/keeper/distribute.go b/x/incentives/keeper/distribute.go index 65fe6ec8b..bd4975605 100644 --- a/x/incentives/keeper/distribute.go +++ b/x/incentives/keeper/distribute.go @@ -4,16 +4,35 @@ import ( "fmt" errorsmod "cosmossdk.io/errors" - + sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/dymensionxyz/dymension/v3/x/incentives/types" - sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/dymensionxyz/dymension/v3/x/incentives/types" ) -// Distribute distributes coins from an array of gauges. +// DistributeOnEpochEnd distributes coins from an array of gauges. // It is called at the end of each epoch to distribute coins to the gauges that are active at that time. -func (k Keeper) Distribute(ctx sdk.Context, gauges []types.Gauge) (sdk.Coins, error) { +func (k Keeper) DistributeOnEpochEnd(ctx sdk.Context, gauges []types.Gauge) (sdk.Coins, error) { + cache := types.NewDenomLocksCache() + + const EpochEnd = true + totalDistributedCoins, err := k.Distribute(ctx, gauges, cache, EpochEnd) + if err != nil { + return nil, fmt.Errorf("distribute gauges: %w", err) + } + + // call post distribution hooks + k.hooks.AfterEpochDistribution(ctx) + + k.checkFinishedGauges(ctx, gauges) + + return totalDistributedCoins, nil +} + +// Distribute distributes coins from an array of gauges. It may be called either at the end or at the middle of +// the epoch. If it's called at the end, then the FilledEpochs field for every gauge is increased. Also, it uses +// a cache specific for asset gauges that helps reduce the number of x/lockup requests. +func (k Keeper) Distribute(ctx sdk.Context, gauges []types.Gauge, cache types.DenomLocksCache, epochEnd bool) (sdk.Coins, error) { lockHolders := newDistributionInfo() totalDistributedCoins := sdk.Coins{} @@ -24,7 +43,8 @@ func (k Keeper) Distribute(ctx sdk.Context, gauges []types.Gauge) (sdk.Coins, er ) switch gauge.DistributeTo.(type) { case *types.Gauge_Asset: - gaugeDistributedCoins, err = k.distributeToAssetGauge(ctx, gauge, &lockHolders) + filteredLocks := k.GetDistributeToBaseLocks(ctx, gauge, cache) + gaugeDistributedCoins, err = k.distributeToAssetGauge(ctx, gauge, filteredLocks, &lockHolders) case *types.Gauge_Rollapp: gaugeDistributedCoins, err = k.distributeToRollappGauge(ctx, gauge) default: @@ -34,7 +54,13 @@ func (k Keeper) Distribute(ctx sdk.Context, gauges []types.Gauge) (sdk.Coins, er return nil, err } - totalDistributedCoins = totalDistributedCoins.Add(gaugeDistributedCoins...) + if !gaugeDistributedCoins.Empty() { + err = k.updateGaugePostDistribute(ctx, gauge, gaugeDistributedCoins, epochEnd) + if err != nil { + return nil, err + } + totalDistributedCoins = totalDistributedCoins.Add(gaugeDistributedCoins...) + } } // apply the distribution to asset gauges @@ -43,10 +69,6 @@ func (k Keeper) Distribute(ctx sdk.Context, gauges []types.Gauge) (sdk.Coins, er return nil, err } - // call post distribution hooks - k.hooks.AfterEpochDistribution(ctx) - - k.checkFinishedGauges(ctx, gauges) return totalDistributedCoins, nil } @@ -88,8 +110,10 @@ func (k Keeper) getToDistributeCoinsFromGauges(gauges []types.Gauge) sdk.Coins { // updateGaugePostDistribute increments the gauge's filled epochs field. // Also adds the coins that were just distributed to the gauge's distributed coins field. -func (k Keeper) updateGaugePostDistribute(ctx sdk.Context, gauge types.Gauge, newlyDistributedCoins sdk.Coins) error { - gauge.FilledEpochs += 1 +func (k Keeper) updateGaugePostDistribute(ctx sdk.Context, gauge types.Gauge, newlyDistributedCoins sdk.Coins, epochEnd bool) error { + if epochEnd { + gauge.FilledEpochs += 1 + } gauge.DistributedCoins = gauge.DistributedCoins.Add(newlyDistributedCoins...) if err := k.setGauge(ctx, &gauge); err != nil { return err diff --git a/x/incentives/keeper/distribute_test.go b/x/incentives/keeper/distribute_test.go index b80a9c659..87d58b980 100644 --- a/x/incentives/keeper/distribute_test.go +++ b/x/incentives/keeper/distribute_test.go @@ -4,10 +4,9 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/suite" - "github.com/dymensionxyz/dymension/v3/x/incentives/types" lockuptypes "github.com/dymensionxyz/dymension/v3/x/lockup/types" + "github.com/stretchr/testify/suite" ) var _ = suite.TestingSuite(nil) @@ -80,7 +79,7 @@ func (suite *KeeperTestSuite) TestDistribute() { // setup gauges and the locks defined in the above tests, then distribute to them gauges := suite.SetupGauges(tc.gauges, defaultLPDenom) addrs := suite.SetupUserLocks(tc.users) - _, err := suite.App.IncentivesKeeper.Distribute(suite.Ctx, gauges) + _, err := suite.App.IncentivesKeeper.DistributeOnEpochEnd(suite.Ctx, gauges) suite.Require().NoError(err) // check expected rewards against actual rewards received for i, addr := range addrs { @@ -125,7 +124,7 @@ func (suite *KeeperTestSuite) TestGetModuleToDistributeCoins() { suite.Require().NoError(err) // distribute coins to stakers - distrCoins, err := suite.App.IncentivesKeeper.Distribute(suite.Ctx, []types.Gauge{*gauge}) + distrCoins, err := suite.App.IncentivesKeeper.DistributeOnEpochEnd(suite.Ctx, []types.Gauge{*gauge}) suite.Require().NoError(err) suite.Require().Equal(distrCoins, sdk.Coins{sdk.NewInt64Coin("stake", 105)}) @@ -157,7 +156,7 @@ func (suite *KeeperTestSuite) TestGetModuleDistributedCoins() { suite.Require().NoError(err) // distribute coins to stakers - distrCoins, err := suite.App.IncentivesKeeper.Distribute(suite.Ctx, []types.Gauge{*gauge}) + distrCoins, err := suite.App.IncentivesKeeper.DistributeOnEpochEnd(suite.Ctx, []types.Gauge{*gauge}) suite.Require().NoError(err) suite.Require().Equal(distrCoins, sdk.Coins{sdk.NewInt64Coin("stake", 5)}) @@ -203,7 +202,7 @@ func (suite *KeeperTestSuite) TestNoLockPerpetualGaugeDistribution() { suite.Require().NoError(err) // distribute coins to stakers, since it's perpetual distribute everything on single distribution - distrCoins, err := suite.App.IncentivesKeeper.Distribute(suite.Ctx, []types.Gauge{*gauge}) + distrCoins, err := suite.App.IncentivesKeeper.DistributeOnEpochEnd(suite.Ctx, []types.Gauge{*gauge}) suite.Require().NoError(err) suite.Require().Equal(distrCoins, sdk.Coins{}) @@ -250,7 +249,7 @@ func (suite *KeeperTestSuite) TestNoLockNonPerpetualGaugeDistribution() { suite.Require().NoError(err) // distribute coins to stakers - distrCoins, err := suite.App.IncentivesKeeper.Distribute(suite.Ctx, []types.Gauge{*gauge}) + distrCoins, err := suite.App.IncentivesKeeper.DistributeOnEpochEnd(suite.Ctx, []types.Gauge{*gauge}) suite.Require().NoError(err) suite.Require().Equal(distrCoins, sdk.Coins{}) diff --git a/x/incentives/keeper/gauge.go b/x/incentives/keeper/gauge.go index eb9b61e95..1d97a7c41 100644 --- a/x/incentives/keeper/gauge.go +++ b/x/incentives/keeper/gauge.go @@ -159,16 +159,6 @@ func (k Keeper) AddToGaugeRewards(ctx sdk.Context, owner sdk.AccAddress, coins s return nil } -// AddToGaugeRewardsByID adds coins to gauge. -// TODO: Used only in x/streamer. Delete after https://github.com/dymensionxyz/dymension/pull/1173 is merged! -func (k Keeper) AddToGaugeRewardsByID(ctx sdk.Context, owner sdk.AccAddress, coins sdk.Coins, gaugeID uint64) error { - gauge, err := k.GetGaugeByID(ctx, gaugeID) - if err != nil { - return err - } - return k.AddToGaugeRewards(ctx, owner, coins, gauge) -} - // GetGaugeByID returns gauge from gauge ID. func (k Keeper) GetGaugeByID(ctx sdk.Context, gaugeID uint64) (*types.Gauge, error) { gauge := types.Gauge{} diff --git a/x/incentives/keeper/gauge_asset.go b/x/incentives/keeper/gauge_asset.go index c36360014..ddd70e1fe 100644 --- a/x/incentives/keeper/gauge_asset.go +++ b/x/incentives/keeper/gauge_asset.go @@ -101,15 +101,12 @@ func (k Keeper) sendRewardsToLocks(ctx sdk.Context, distrs *distributionInfo) er // distributeToAssetGauge runs the distribution logic for a gauge, and adds the sends to // the distrInfo struct. It also updates the gauge for the distribution. // Locks is expected to be the correct set of lock recipients for this gauge. -func (k Keeper) distributeToAssetGauge(ctx sdk.Context, gauge types.Gauge, currResult *distributionInfo) (sdk.Coins, error) { +func (k Keeper) distributeToAssetGauge(ctx sdk.Context, gauge types.Gauge, locks []lockuptypes.PeriodLock, currResult *distributionInfo) (sdk.Coins, error) { assetDist := gauge.GetAsset() if assetDist == nil { return sdk.Coins{}, fmt.Errorf("gauge %d is not an asset gauge", gauge.Id) } - locksByDenomCache := make(map[string][]lockuptypes.PeriodLock) - locks := k.getDistributeToBaseLocks(ctx, gauge, locksByDenomCache) - denom := assetDist.Denom lockSum := lockuptypes.SumLocksByDenom(locks, denom) @@ -135,8 +132,7 @@ func (k Keeper) distributeToAssetGauge(ctx sdk.Context, gauge types.Gauge, currR // this should never happen in practice if remainCoins.Empty() { ctx.Logger().Error(fmt.Sprintf("gauge %d is empty, skipping", gauge.Id)) - err := k.updateGaugePostDistribute(ctx, gauge, sdk.Coins{}) - return sdk.Coins{}, err + return sdk.Coins{}, nil } totalDistrCoins := sdk.NewCoins() @@ -164,12 +160,11 @@ func (k Keeper) distributeToAssetGauge(ctx sdk.Context, gauge types.Gauge, currR totalDistrCoins = totalDistrCoins.Add(distrCoins...) } - err := k.updateGaugePostDistribute(ctx, gauge, totalDistrCoins) - return totalDistrCoins, err + return totalDistrCoins, nil } -// getDistributeToBaseLocks takes a gauge along with cached period locks by denom and returns locks that must be distributed to -func (k Keeper) getDistributeToBaseLocks(ctx sdk.Context, gauge types.Gauge, cache map[string][]lockuptypes.PeriodLock) []lockuptypes.PeriodLock { +// GetDistributeToBaseLocks takes a gauge along with cached period locks by denom and returns locks that must be distributed to +func (k Keeper) GetDistributeToBaseLocks(ctx sdk.Context, gauge types.Gauge, cache types.DenomLocksCache) []lockuptypes.PeriodLock { // if gauge is empty, don't get the locks if gauge.Coins.Empty() { return []lockuptypes.PeriodLock{} diff --git a/x/incentives/keeper/gauge_rollapp.go b/x/incentives/keeper/gauge_rollapp.go index 5de9f4cfc..b84692290 100644 --- a/x/incentives/keeper/gauge_rollapp.go +++ b/x/incentives/keeper/gauge_rollapp.go @@ -60,10 +60,5 @@ func (k Keeper) distributeToRollappGauge(ctx sdk.Context, gauge types.Gauge) (to return sdk.Coins{}, err } - err = k.updateGaugePostDistribute(ctx, gauge, totalDistrCoins) - if err != nil { - return sdk.Coins{}, err - } - return totalDistrCoins, nil } diff --git a/x/incentives/keeper/gauge_rollapp_test.go b/x/incentives/keeper/gauge_rollapp_test.go index efa7bdc6b..82eff8c64 100644 --- a/x/incentives/keeper/gauge_rollapp_test.go +++ b/x/incentives/keeper/gauge_rollapp_test.go @@ -52,7 +52,7 @@ func (suite *KeeperTestSuite) TestDistributeToRollappGauges() { suite.Require().NoError(err) // Distribute to the rollapp owner - _, err = suite.App.IncentivesKeeper.Distribute(suite.Ctx, []types.Gauge{*gauge}) + _, err = suite.App.IncentivesKeeper.DistributeOnEpochEnd(suite.Ctx, []types.Gauge{*gauge}) suite.Require().NoError(err) // Check expected rewards against actual rewards received @@ -116,7 +116,7 @@ func (suite *KeeperTestSuite) TestDistributeToRollappGaugesAfterOwnerChange() { gauge, err := suite.App.IncentivesKeeper.GetGaugeByID(suite.Ctx, gaugeId) suite.Require().NoError(err) - _, err = suite.App.IncentivesKeeper.Distribute(suite.Ctx, []types.Gauge{*gauge}) + _, err = suite.App.IncentivesKeeper.DistributeOnEpochEnd(suite.Ctx, []types.Gauge{*gauge}) suite.Require().NoError(err) } @@ -132,7 +132,7 @@ func (suite *KeeperTestSuite) TestDistributeToRollappGaugesAfterOwnerChange() { gauge, err := suite.App.IncentivesKeeper.GetGaugeByID(suite.Ctx, gaugeId) suite.Require().NoError(err) - _, err = suite.App.IncentivesKeeper.Distribute(suite.Ctx, []types.Gauge{*gauge}) + _, err = suite.App.IncentivesKeeper.DistributeOnEpochEnd(suite.Ctx, []types.Gauge{*gauge}) suite.Require().NoError(err) } diff --git a/x/incentives/keeper/gauge_test.go b/x/incentives/keeper/gauge_test.go index 4a13041af..5f4916582 100644 --- a/x/incentives/keeper/gauge_test.go +++ b/x/incentives/keeper/gauge_test.go @@ -150,7 +150,7 @@ func (suite *KeeperTestSuite) TestGaugeOperations() { suite.Require().Len(gaugeIdsByDenom, 0) // distribute coins to stakers - distrCoins, err := suite.App.IncentivesKeeper.Distribute(suite.Ctx, []types.Gauge{*gauge}) + distrCoins, err := suite.App.IncentivesKeeper.DistributeOnEpochEnd(suite.Ctx, []types.Gauge{*gauge}) suite.Require().NoError(err) // We hardcoded 12 "stake" tokens when initializing gauge suite.Require().Equal(sdk.Coins{sdk.NewInt64Coin("stake", int64(12/expectedNumEpochsPaidOver))}, distrCoins) @@ -159,7 +159,7 @@ func (suite *KeeperTestSuite) TestGaugeOperations() { // distributing twice without adding more for perpetual gauge gauge, err = suite.App.IncentivesKeeper.GetGaugeByID(suite.Ctx, gaugeID) suite.Require().NoError(err) - distrCoins, err = suite.App.IncentivesKeeper.Distribute(suite.Ctx, []types.Gauge{*gauge}) + distrCoins, err = suite.App.IncentivesKeeper.DistributeOnEpochEnd(suite.Ctx, []types.Gauge{*gauge}) suite.Require().NoError(err) suite.Require().True(distrCoins.Empty()) @@ -170,7 +170,7 @@ func (suite *KeeperTestSuite) TestGaugeOperations() { // distributing twice with adding more for perpetual gauge gauge, err = suite.App.IncentivesKeeper.GetGaugeByID(suite.Ctx, gaugeID) suite.Require().NoError(err) - distrCoins, err = suite.App.IncentivesKeeper.Distribute(suite.Ctx, []types.Gauge{*gauge}) + distrCoins, err = suite.App.IncentivesKeeper.DistributeOnEpochEnd(suite.Ctx, []types.Gauge{*gauge}) suite.Require().NoError(err) suite.Require().Equal(sdk.Coins{sdk.NewInt64Coin("stake", 200)}, distrCoins) } else { diff --git a/x/incentives/keeper/grpc_query_test.go b/x/incentives/keeper/grpc_query_test.go index 257f07ea6..91e5c95f3 100644 --- a/x/incentives/keeper/grpc_query_test.go +++ b/x/incentives/keeper/grpc_query_test.go @@ -354,7 +354,7 @@ func (suite *KeeperTestSuite) TestGRPCToDistributeCoins() { suite.Require().Equal(res.Coins, coins) // distribute coins to stakers - distrCoins, err := suite.querier.Distribute(suite.Ctx, gauges) + distrCoins, err := suite.querier.DistributeOnEpochEnd(suite.Ctx, gauges) suite.Require().NoError(err) suite.Require().Equal(distrCoins, sdk.Coins{sdk.NewInt64Coin("stake", 4)}) @@ -379,7 +379,7 @@ func (suite *KeeperTestSuite) TestGRPCToDistributeCoins() { suite.Require().Equal(res.Coins, coins.Sub(distrCoins...)) // distribute second round to stakers - distrCoins, err = suite.querier.Distribute(suite.Ctx, gauges) + distrCoins, err = suite.querier.DistributeOnEpochEnd(suite.Ctx, gauges) suite.Require().NoError(err) suite.Require().Equal(sdk.Coins{sdk.NewInt64Coin("stake", 6)}, distrCoins) @@ -411,7 +411,7 @@ func (suite *KeeperTestSuite) TestGRPCDistributedCoins() { suite.Require().NoError(err) // distribute coins to stakers - distrCoins, err := suite.querier.Distribute(suite.Ctx, gauges) + distrCoins, err := suite.querier.DistributeOnEpochEnd(suite.Ctx, gauges) suite.Require().NoError(err) suite.Require().Equal(distrCoins, sdk.Coins{sdk.NewInt64Coin("stake", 4)}) @@ -426,7 +426,7 @@ func (suite *KeeperTestSuite) TestGRPCDistributedCoins() { gauges = []types.Gauge{*gauge} // distribute second round to stakers - distrCoins, err = suite.querier.Distribute(suite.Ctx, gauges) + distrCoins, err = suite.querier.DistributeOnEpochEnd(suite.Ctx, gauges) suite.Require().NoError(err) suite.Require().Equal(sdk.Coins{sdk.NewInt64Coin("stake", 6)}, distrCoins) } diff --git a/x/incentives/keeper/hooks.go b/x/incentives/keeper/hooks.go index 50b521fe3..ee8d8b136 100644 --- a/x/incentives/keeper/hooks.go +++ b/x/incentives/keeper/hooks.go @@ -31,7 +31,7 @@ func (k Keeper) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, epochNumb // distribute due to epoch event gauges = k.GetActiveGauges(ctx) - _, err := k.Distribute(ctx, gauges) + _, err := k.DistributeOnEpochEnd(ctx, gauges) if err != nil { return err } diff --git a/x/incentives/types/types.go b/x/incentives/types/types.go new file mode 100644 index 000000000..7a1f740f2 --- /dev/null +++ b/x/incentives/types/types.go @@ -0,0 +1,23 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + lockuptypes "github.com/dymensionxyz/dymension/v3/x/lockup/types" +) + +// DenomLocksCache is a cache that persists the list of lockups per denom, so +// a string key is a denom name. +type DenomLocksCache map[string][]lockuptypes.PeriodLock + +func NewDenomLocksCache() DenomLocksCache { + return make(DenomLocksCache) +} + +func (g *Gauge) AddCoins(coins sdk.Coins) { + g.Coins = g.Coins.Add(coins...) +} + +func (g Gauge) Key() uint64 { + return g.Id +} diff --git a/x/streamer/keeper/abci.go b/x/streamer/keeper/abci.go index c86de13f1..b73a8b046 100644 --- a/x/streamer/keeper/abci.go +++ b/x/streamer/keeper/abci.go @@ -9,52 +9,26 @@ import ( "github.com/dymensionxyz/dymension/v3/x/streamer/types" ) +// EndBlock iterates over the epoch pointers, calculates rewards, distributes them, and updates the streams. func (k Keeper) EndBlock(ctx sdk.Context) error { - streams := k.GetActiveStreams(ctx) - epochPointers, err := k.GetAllEpochPointers(ctx) if err != nil { return fmt.Errorf("get all epoch pointers: %w", err) } - // Sort epoch pointers to distribute to shorter epochs first - types.SortEpochPointers(epochPointers) - + streams := k.GetActiveStreams(ctx) maxIterations := k.GetParams(ctx).MaxIterationsPerBlock - totalIterations := uint64(0) - totalDistributed := sdk.NewCoins() - - for _, p := range epochPointers { - remainIterations := maxIterations - totalIterations - - if remainIterations <= 0 { - break // no more iterations available for this block - } - result := k.DistributeRewards(ctx, p, remainIterations, streams) - - totalIterations += result.Iterations - totalDistributed = totalDistributed.Add(result.DistributedCoins...) - streams = result.FilledStreams - - err = k.SaveEpochPointer(ctx, result.NewPointer) - if err != nil { - return fmt.Errorf("save epoch pointer: %w", err) - } - } - - // Save stream updates - for _, stream := range streams { - err = k.SetStream(ctx, &stream) - if err != nil { - return fmt.Errorf("set stream: %w", err) - } + const epochEnd = false + coins, iterations, err := k.Distribute(ctx, epochPointers, streams, maxIterations, epochEnd) + if err != nil { + return fmt.Errorf("distribute: %w", err) } err = uevent.EmitTypedEvent(ctx, &types.EventEndBlock{ - Iterations: totalIterations, + Iterations: iterations, MaxIterations: maxIterations, - Distributed: totalDistributed, + Distributed: coins, }) if err != nil { return fmt.Errorf("emit typed event: %w", err) diff --git a/x/streamer/keeper/abci_test.go b/x/streamer/keeper/abci_test.go index 6ef87c046..d3df98626 100644 --- a/x/streamer/keeper/abci_test.go +++ b/x/streamer/keeper/abci_test.go @@ -7,73 +7,888 @@ import ( "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/dymensionxyz/dymension/v3/app/apptesting" "github.com/dymensionxyz/dymension/v3/x/streamer/types" ) func (s *KeeperTestSuite) TestProcessEpochPointer() { + addrs := apptesting.CreateRandomAccounts(4) tests := []struct { name string maxIterationsPerBlock uint64 numGauges int - blocksInEpoch int + blocksToProcess int + initialLockups []lockup streams []types.Stream expectedBlockResults []blockResults }{ { - name: "1 block in the epoch", + // In this test, the number of gauges is less than the number of iterations. We simulate the + // execution of the first block of the epoch: + // 1. There are 4 streams, and each streams holds 200 stake + // 2. Each stream has 4 gauges with 25% weight each => the number of gauges is 16 + // 3. We start with shorter epochs, so firstly we fill streams with the hour epoch (1 and 4) + // 4. After, we continue with the longer streams (2 and 3) + // 5. There are 9 iterations limit per block, so we fill first 9 gauges: + // * 4 gauges from stream 1 + // * 4 gauges from stream 4 + // * 1 gauge from stream 2 + // 6. Each gauge gets 25% of the stream => 50 stake => 50 * 9 = 450 stake is totally distributed + // 7. Initially, we have 1 lockup owner with 100 stake locked, so it gets 100% of rewards + // of the stake denom => every owner will get 450 stake + name: "1 block in the epoch, iteration weight = 1 (one lockup)", maxIterationsPerBlock: 9, numGauges: 16, - blocksInEpoch: 1, + blocksToProcess: 1, + initialLockups: []lockup{ // this owner receives 100% of the rewards + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + }, + streams: []types.Stream{ + { + Id: 1, + DistrEpochIdentifier: "hour", + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ + Records: []types.DistrRecord{ + {GaugeId: 1, Weight: math.NewInt(50)}, + {GaugeId: 2, Weight: math.NewInt(50)}, + {GaugeId: 3, Weight: math.NewInt(50)}, + {GaugeId: 4, Weight: math.NewInt(50)}, + }, + }, + }, + { + Id: 2, + DistrEpochIdentifier: "day", + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ + Records: []types.DistrRecord{ + {GaugeId: 5, Weight: math.NewInt(50)}, + {GaugeId: 6, Weight: math.NewInt(50)}, + {GaugeId: 7, Weight: math.NewInt(50)}, + {GaugeId: 8, Weight: math.NewInt(50)}, + }, + }, + }, + { + Id: 3, + DistrEpochIdentifier: "day", + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ + Records: []types.DistrRecord{ + {GaugeId: 9, Weight: math.NewInt(50)}, + {GaugeId: 10, Weight: math.NewInt(50)}, + {GaugeId: 11, Weight: math.NewInt(50)}, + {GaugeId: 12, Weight: math.NewInt(50)}, + }, + }, + }, + { + Id: 4, + DistrEpochIdentifier: "hour", + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ + Records: []types.DistrRecord{ + {GaugeId: 13, Weight: math.NewInt(50)}, + {GaugeId: 14, Weight: math.NewInt(50)}, + {GaugeId: 15, Weight: math.NewInt(50)}, + {GaugeId: 16, Weight: math.NewInt(50)}, + }, + }, + }, + }, + expectedBlockResults: []blockResults{ + { + height: 0, + epochPointers: []types.EpochPointer{ + { + StreamId: types.MaxStreamID, + GaugeId: types.MaxStreamID, + EpochIdentifier: "hour", + EpochDuration: time.Hour, + }, + { + StreamId: 2, + GaugeId: 6, + EpochIdentifier: "day", + EpochDuration: 24 * time.Hour, + }, + // week epoch pointer is not used + { + StreamId: types.MinStreamID, + GaugeId: types.MinGaugeID, + EpochIdentifier: "week", + EpochDuration: 7 * 24 * time.Hour, + }, + }, + distributedCoins: []distributedCoins{ + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {streamID: 3, coins: nil}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + }, + gauges: []gaugeCoins{ + // 1st stream + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 2nd stream + {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 6, coins: nil}, + {gaugeID: 7, coins: nil}, + {gaugeID: 8, coins: nil}, + // 3rd stream + {gaugeID: 9, coins: nil}, + {gaugeID: 10, coins: nil}, + {gaugeID: 11, coins: nil}, + {gaugeID: 12, coins: nil}, + // 4th stream + {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 450))}, + }, + }, + }, + }, + { + name: "Several blocks in the epoch, iteration weight = 1 (one lockup)", + maxIterationsPerBlock: 5, + numGauges: 16, + blocksToProcess: 2, + initialLockups: []lockup{ // this owner receives 100% of the rewards + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + }, + streams: []types.Stream{ + { + Id: 1, + DistrEpochIdentifier: "hour", + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ + Records: []types.DistrRecord{ + {GaugeId: 1, Weight: math.NewInt(50)}, + {GaugeId: 2, Weight: math.NewInt(50)}, + {GaugeId: 3, Weight: math.NewInt(50)}, + {GaugeId: 4, Weight: math.NewInt(50)}, + }, + }, + }, + { + Id: 2, + DistrEpochIdentifier: "day", + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ + Records: []types.DistrRecord{ + {GaugeId: 5, Weight: math.NewInt(50)}, + {GaugeId: 6, Weight: math.NewInt(50)}, + {GaugeId: 7, Weight: math.NewInt(50)}, + {GaugeId: 8, Weight: math.NewInt(50)}, + }, + }, + }, + { + Id: 3, + DistrEpochIdentifier: "day", + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ + Records: []types.DistrRecord{ + {GaugeId: 9, Weight: math.NewInt(50)}, + {GaugeId: 10, Weight: math.NewInt(50)}, + {GaugeId: 11, Weight: math.NewInt(50)}, + {GaugeId: 12, Weight: math.NewInt(50)}, + }, + }, + }, + { + Id: 4, + DistrEpochIdentifier: "hour", + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ + Records: []types.DistrRecord{ + {GaugeId: 13, Weight: math.NewInt(50)}, + {GaugeId: 14, Weight: math.NewInt(50)}, + {GaugeId: 15, Weight: math.NewInt(50)}, + {GaugeId: 16, Weight: math.NewInt(50)}, + }, + }, + }, + }, + expectedBlockResults: []blockResults{ + { + height: 0, + epochPointers: []types.EpochPointer{ + { + StreamId: 4, + GaugeId: 14, + EpochIdentifier: "hour", + EpochDuration: time.Hour, + }, + { + StreamId: 0, + GaugeId: 0, + EpochIdentifier: "day", + EpochDuration: 24 * time.Hour, + }, + // week epoch pointer is not used + { + StreamId: types.MinStreamID, + GaugeId: types.MinGaugeID, + EpochIdentifier: "week", + EpochDuration: 7 * 24 * time.Hour, + }, + }, + distributedCoins: []distributedCoins{ + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: nil}, + {streamID: 3, coins: nil}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + }, + gauges: []gaugeCoins{ + // 1st stream + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 2nd stream + {gaugeID: 5, coins: nil}, + {gaugeID: 6, coins: nil}, + {gaugeID: 7, coins: nil}, + {gaugeID: 8, coins: nil}, + // 3rd stream + {gaugeID: 9, coins: nil}, + {gaugeID: 10, coins: nil}, + {gaugeID: 11, coins: nil}, + {gaugeID: 12, coins: nil}, + // 4th stream + {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 14, coins: nil}, + {gaugeID: 15, coins: nil}, + {gaugeID: 16, coins: nil}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 250))}, + }, + }, + { + height: 1, + epochPointers: []types.EpochPointer{ + { + StreamId: types.MaxStreamID, + GaugeId: types.MaxStreamID, + EpochIdentifier: "hour", + EpochDuration: time.Hour, + }, + { + StreamId: 2, + GaugeId: 7, + EpochIdentifier: "day", + EpochDuration: 24 * time.Hour, + }, + // week epoch pointer is not used + { + StreamId: types.MinStreamID, + GaugeId: types.MinGaugeID, + EpochIdentifier: "week", + EpochDuration: 7 * 24 * time.Hour, + }, + }, + distributedCoins: []distributedCoins{ + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + {streamID: 3, coins: nil}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + }, + gauges: []gaugeCoins{ + // 1st stream + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 2nd stream + {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 6, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 7, coins: nil}, + {gaugeID: 8, coins: nil}, + // 3rd stream + {gaugeID: 9, coins: nil}, + {gaugeID: 10, coins: nil}, + {gaugeID: 11, coins: nil}, + {gaugeID: 12, coins: nil}, + // 4th stream + {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 500))}, + }, + }, + }, + }, + { + name: "Send all reward in one single block, iteration weight = 1 (one lockup)", + maxIterationsPerBlock: 5, + numGauges: 4, + blocksToProcess: 5, + initialLockups: []lockup{ // this owner receives 100% of the rewards + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + }, + streams: []types.Stream{ + { + Id: 1, + DistrEpochIdentifier: "hour", + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2)), + DistributeTo: types.DistrInfo{ + Records: []types.DistrRecord{ + {GaugeId: 1, Weight: math.NewInt(1)}, + }, + }, + }, + { + Id: 2, + DistrEpochIdentifier: "day", + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2)), + DistributeTo: types.DistrInfo{ + Records: []types.DistrRecord{ + {GaugeId: 2, Weight: math.NewInt(1)}, + }, + }, + }, + { + Id: 3, + DistrEpochIdentifier: "day", + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2)), + DistributeTo: types.DistrInfo{ + Records: []types.DistrRecord{ + {GaugeId: 3, Weight: math.NewInt(1)}, + }, + }, + }, + { + Id: 4, + DistrEpochIdentifier: "hour", + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2)), + DistributeTo: types.DistrInfo{ + Records: []types.DistrRecord{ + {GaugeId: 4, Weight: math.NewInt(1)}, + }, + }, + }, + }, + expectedBlockResults: []blockResults{ + { + height: 0, + epochPointers: []types.EpochPointer{ + { + StreamId: types.MaxStreamID, + GaugeId: types.MaxStreamID, + EpochIdentifier: "hour", + EpochDuration: time.Hour, + }, + { + StreamId: types.MaxStreamID, + GaugeId: types.MaxStreamID, + EpochIdentifier: "day", + EpochDuration: 24 * time.Hour, + }, + // week epoch pointer is not used, however it points on the last gauge + { + StreamId: types.MaxStreamID, + GaugeId: types.MaxStreamID, + EpochIdentifier: "week", + EpochDuration: 7 * 24 * time.Hour, + }, + }, + distributedCoins: []distributedCoins{ + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2))}, + {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2))}, + {streamID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2))}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2))}, + }, + gauges: []gaugeCoins{ + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2))}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 8))}, + }, + }, + }, + }, + { + name: "Many blocks, iteration weight = 1 (one lockup)", + maxIterationsPerBlock: 3, + numGauges: 16, + blocksToProcess: 200, + initialLockups: []lockup{ // this owner receives 100% of the rewards + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + }, + streams: []types.Stream{ + { + Id: 1, + DistrEpochIdentifier: "hour", + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ + Records: []types.DistrRecord{ + {GaugeId: 1, Weight: math.NewInt(50)}, + {GaugeId: 2, Weight: math.NewInt(50)}, + {GaugeId: 3, Weight: math.NewInt(50)}, + {GaugeId: 4, Weight: math.NewInt(50)}, + }, + }, + }, + { + Id: 2, + DistrEpochIdentifier: "day", + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ + Records: []types.DistrRecord{ + {GaugeId: 5, Weight: math.NewInt(50)}, + {GaugeId: 6, Weight: math.NewInt(50)}, + {GaugeId: 7, Weight: math.NewInt(50)}, + {GaugeId: 8, Weight: math.NewInt(50)}, + }, + }, + }, + { + Id: 3, + DistrEpochIdentifier: "day", + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ + Records: []types.DistrRecord{ + {GaugeId: 9, Weight: math.NewInt(50)}, + {GaugeId: 10, Weight: math.NewInt(50)}, + {GaugeId: 11, Weight: math.NewInt(50)}, + {GaugeId: 12, Weight: math.NewInt(50)}, + }, + }, + }, + { + Id: 4, + DistrEpochIdentifier: "hour", + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ + Records: []types.DistrRecord{ + {GaugeId: 13, Weight: math.NewInt(50)}, + {GaugeId: 14, Weight: math.NewInt(50)}, + {GaugeId: 15, Weight: math.NewInt(50)}, + {GaugeId: 16, Weight: math.NewInt(50)}, + }, + }, + }, + }, + expectedBlockResults: []blockResults{ + { + height: 0, + epochPointers: []types.EpochPointer{ + { + StreamId: 1, + GaugeId: 4, + EpochIdentifier: "hour", + EpochDuration: time.Hour, + }, + { + StreamId: 0, + GaugeId: 0, + EpochIdentifier: "day", + EpochDuration: 24 * time.Hour, + }, + // week epoch pointer is not used + { + StreamId: types.MinStreamID, + GaugeId: types.MinGaugeID, + EpochIdentifier: "week", + EpochDuration: 7 * 24 * time.Hour, + }, + }, + distributedCoins: []distributedCoins{ + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 150))}, + {streamID: 2, coins: nil}, + {streamID: 3, coins: nil}, + {streamID: 4, coins: nil}, + }, + gauges: []gaugeCoins{ + // 1st stream + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: nil}, + // 2nd stream + {gaugeID: 5, coins: nil}, + {gaugeID: 6, coins: nil}, + {gaugeID: 7, coins: nil}, + {gaugeID: 8, coins: nil}, + // 3rd stream + {gaugeID: 9, coins: nil}, + {gaugeID: 10, coins: nil}, + {gaugeID: 11, coins: nil}, + {gaugeID: 12, coins: nil}, + // 4th stream + {gaugeID: 13, coins: nil}, + {gaugeID: 14, coins: nil}, + {gaugeID: 15, coins: nil}, + {gaugeID: 16, coins: nil}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 150))}, + }, + }, + { + height: 1, + epochPointers: []types.EpochPointer{ + { + StreamId: 4, + GaugeId: 15, + EpochIdentifier: "hour", + EpochDuration: time.Hour, + }, + { + StreamId: 0, + GaugeId: 0, + EpochIdentifier: "day", + EpochDuration: 24 * time.Hour, + }, + // week epoch pointer is not used + { + StreamId: types.MinStreamID, + GaugeId: types.MinGaugeID, + EpochIdentifier: "week", + EpochDuration: 7 * 24 * time.Hour, + }, + }, + distributedCoins: []distributedCoins{ + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: nil}, + {streamID: 3, coins: nil}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + }, + gauges: []gaugeCoins{ + // 1st stream + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 2nd stream + {gaugeID: 5, coins: nil}, + {gaugeID: 6, coins: nil}, + {gaugeID: 7, coins: nil}, + {gaugeID: 8, coins: nil}, + // 3rd stream + {gaugeID: 9, coins: nil}, + {gaugeID: 10, coins: nil}, + {gaugeID: 11, coins: nil}, + {gaugeID: 12, coins: nil}, + // 4th stream + {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 15, coins: nil}, + {gaugeID: 16, coins: nil}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 300))}, + }, + }, + { + height: 2, + epochPointers: []types.EpochPointer{ + { + StreamId: types.MaxStreamID, + GaugeId: types.MaxGaugeID, + EpochIdentifier: "hour", + EpochDuration: time.Hour, + }, + { + StreamId: 2, + GaugeId: 6, + EpochIdentifier: "day", + EpochDuration: 24 * time.Hour, + }, + // week epoch pointer is not used + { + StreamId: types.MinStreamID, + GaugeId: types.MinGaugeID, + EpochIdentifier: "week", + EpochDuration: 7 * 24 * time.Hour, + }, + }, + distributedCoins: []distributedCoins{ + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {streamID: 3, coins: nil}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + }, + gauges: []gaugeCoins{ + // 1st stream + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 2nd stream + {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 6, coins: nil}, + {gaugeID: 7, coins: nil}, + {gaugeID: 8, coins: nil}, + // 3rd stream + {gaugeID: 9, coins: nil}, + {gaugeID: 10, coins: nil}, + {gaugeID: 11, coins: nil}, + {gaugeID: 12, coins: nil}, + // 4th stream + {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 450))}, + }, + }, + { + height: 3, + epochPointers: []types.EpochPointer{ + { + StreamId: types.MaxStreamID, + GaugeId: types.MaxGaugeID, + EpochIdentifier: "hour", + EpochDuration: time.Hour, + }, + { + StreamId: 3, + GaugeId: 9, + EpochIdentifier: "day", + EpochDuration: 24 * time.Hour, + }, + // week epoch pointer is not used + { + StreamId: types.MinStreamID, + GaugeId: types.MinGaugeID, + EpochIdentifier: "week", + EpochDuration: 7 * 24 * time.Hour, + }, + }, + distributedCoins: []distributedCoins{ + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 3, coins: nil}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + }, + gauges: []gaugeCoins{ + // 1st stream + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 2nd stream + {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 6, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 7, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 8, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 3rd stream + {gaugeID: 9, coins: nil}, + {gaugeID: 10, coins: nil}, + {gaugeID: 11, coins: nil}, + {gaugeID: 12, coins: nil}, + // 4th stream + {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 600))}, + }, + }, + { + height: 3, + epochPointers: []types.EpochPointer{ + { + StreamId: types.MaxStreamID, + GaugeId: types.MaxGaugeID, + EpochIdentifier: "hour", + EpochDuration: time.Hour, + }, + { + StreamId: 3, + GaugeId: 12, + EpochIdentifier: "day", + EpochDuration: 24 * time.Hour, + }, + // week epoch pointer is not used + { + StreamId: types.MinStreamID, + GaugeId: types.MinGaugeID, + EpochIdentifier: "week", + EpochDuration: 7 * 24 * time.Hour, + }, + }, + distributedCoins: []distributedCoins{ + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 150))}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + }, + gauges: []gaugeCoins{ + // 1st stream + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 2nd stream + {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 6, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 7, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 8, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 3rd stream + {gaugeID: 9, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 10, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 11, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 12, coins: nil}, + // 4th stream + {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 750))}, + }, + }, + { + height: 4, + epochPointers: []types.EpochPointer{ + { + StreamId: types.MaxStreamID, + GaugeId: types.MaxGaugeID, + EpochIdentifier: "hour", + EpochDuration: time.Hour, + }, + { + StreamId: types.MaxStreamID, + GaugeId: types.MaxStreamID, + EpochIdentifier: "day", + EpochDuration: 24 * time.Hour, + }, + // week epoch pointer is not used, however it points on the last gauge + { + StreamId: types.MaxStreamID, + GaugeId: types.MaxStreamID, + EpochIdentifier: "week", + EpochDuration: 7 * 24 * time.Hour, + }, + }, + distributedCoins: []distributedCoins{ + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + }, + gauges: []gaugeCoins{ + // 1st stream + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 2nd stream + {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 6, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 7, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 8, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 3rd stream + {gaugeID: 9, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 10, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 11, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 12, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 4th stream + {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 800))}, + }, + }, + }, + }, + { + // In this test, the number of gauges is less than the number of iterations. We simulate the + // execution of the first block of the epoch: + // 1. There are 4 streams, and each streams holds 200 stake + // 2. Each stream has 4 gauges with 25% weight each => the number of gauges is 16 + // 3. We start with shorter epochs, so firstly we fill streams with the hour epoch (1 and 4) + // 4. After, we continue with the longer streams (2 and 3) + // 5. Each gauge has 2 lockups, so every iteration "costs" as 2 + // 5. There are 9 iterations limit per block, so we fill first 5 gauges: + // * 4 gauges from stream 1 + // * 1 gauges from stream 4 + // 6. Each gauge gets 25% of the stream => 50 stake => 50 * 5 = 250 stake is totally distributed + // 7. Initially, we have 2 lockup owners with 100 stake locked each, so each them gets 50% of rewards + // of the stake denom => every owner will get 125 stake + name: "1 block in the epoch, iteration weight = 2 (two lockups)", + maxIterationsPerBlock: 9, + numGauges: 16, + blocksToProcess: 1, + initialLockups: []lockup{ // this owner receives 100% of the rewards + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + }, streams: []types.Stream{ { Id: 1, DistrEpochIdentifier: "hour", - Coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100)), - DistributeTo: &types.DistrInfo{ + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ Records: []types.DistrRecord{ - {GaugeId: 1, Weight: math.NewInt(25)}, - {GaugeId: 2, Weight: math.NewInt(25)}, - {GaugeId: 3, Weight: math.NewInt(25)}, - {GaugeId: 4, Weight: math.NewInt(25)}, + {GaugeId: 1, Weight: math.NewInt(50)}, + {GaugeId: 2, Weight: math.NewInt(50)}, + {GaugeId: 3, Weight: math.NewInt(50)}, + {GaugeId: 4, Weight: math.NewInt(50)}, }, }, }, { Id: 2, DistrEpochIdentifier: "day", - Coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100)), - DistributeTo: &types.DistrInfo{ + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ Records: []types.DistrRecord{ - {GaugeId: 5, Weight: math.NewInt(25)}, - {GaugeId: 6, Weight: math.NewInt(25)}, - {GaugeId: 7, Weight: math.NewInt(25)}, - {GaugeId: 8, Weight: math.NewInt(25)}, + {GaugeId: 5, Weight: math.NewInt(50)}, + {GaugeId: 6, Weight: math.NewInt(50)}, + {GaugeId: 7, Weight: math.NewInt(50)}, + {GaugeId: 8, Weight: math.NewInt(50)}, }, }, }, { Id: 3, DistrEpochIdentifier: "day", - Coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100)), - DistributeTo: &types.DistrInfo{ + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ Records: []types.DistrRecord{ - {GaugeId: 9, Weight: math.NewInt(25)}, - {GaugeId: 10, Weight: math.NewInt(25)}, - {GaugeId: 11, Weight: math.NewInt(25)}, - {GaugeId: 12, Weight: math.NewInt(25)}, + {GaugeId: 9, Weight: math.NewInt(50)}, + {GaugeId: 10, Weight: math.NewInt(50)}, + {GaugeId: 11, Weight: math.NewInt(50)}, + {GaugeId: 12, Weight: math.NewInt(50)}, }, }, }, { Id: 4, DistrEpochIdentifier: "hour", - Coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100)), - DistributeTo: &types.DistrInfo{ + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ Records: []types.DistrRecord{ - {GaugeId: 13, Weight: math.NewInt(25)}, - {GaugeId: 14, Weight: math.NewInt(25)}, - {GaugeId: 15, Weight: math.NewInt(25)}, - {GaugeId: 16, Weight: math.NewInt(25)}, + {GaugeId: 13, Weight: math.NewInt(50)}, + {GaugeId: 14, Weight: math.NewInt(50)}, + {GaugeId: 15, Weight: math.NewInt(50)}, + {GaugeId: 16, Weight: math.NewInt(50)}, }, }, }, @@ -83,39 +898,39 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { height: 0, epochPointers: []types.EpochPointer{ { - StreamId: types.MaxStreamID, - GaugeId: types.MaxStreamID, + StreamId: 4, + GaugeId: 14, EpochIdentifier: "hour", EpochDuration: time.Hour, }, { - StreamId: 2, - GaugeId: 6, + StreamId: types.MinStreamID, + GaugeId: types.MinGaugeID, EpochIdentifier: "day", EpochDuration: 24 * time.Hour, }, // week epoch pointer is not used { StreamId: types.MinStreamID, - GaugeId: types.MinStreamID, + GaugeId: types.MinGaugeID, EpochIdentifier: "week", EpochDuration: 7 * 24 * time.Hour, }, }, distributedCoins: []distributedCoins{ - {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, - {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: nil}, {streamID: 3, coins: nil}, - {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, }, gauges: []gaugeCoins{ // 1st stream - {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, // 2nd stream - {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 5, coins: nil}, {gaugeID: 6, coins: nil}, {gaugeID: 7, coins: nil}, {gaugeID: 8, coins: nil}, @@ -125,69 +940,94 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { {gaugeID: 11, coins: nil}, {gaugeID: 12, coins: nil}, // 4th stream - {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 14, coins: nil}, + {gaugeID: 15, coins: nil}, + {gaugeID: 16, coins: nil}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 125))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 125))}, }, }, }, }, { - name: "Several blocks in the epoch", + // 0. Simulate two blocks, 5 iters per block, each gauge has 4 lockups + // 1. There are 4 streams, and each streams holds 200 stake + // 2. Each stream has 4 gauges with 25% weight each => the number of gauges is 16 + // 3. We start with shorter epochs, so firstly we fill streams with the hour epoch (1 and 4) + // 4. After, we continue with the longer streams (2 and 3) + // 5. Each gauge has 4 lockups, so every iteration "costs" as 4 + // 5. There are 5 iterations limit per block, so we fill first 2 gauges in the first block + // * 2 gauges from stream 1 + // 6. And 2 more gauges in the second block: + // * 2 gauges from stream 1 + // 6. Each gauge gets 25% of the stream => 50 stake => 50 * 4 = 200 stake is totally distributed + // 7. Initially, we have 4 lockup owners with 100 stake locked each, so each them gets 25% of rewards + // of the stake denom => + // * every owner will get 24 stake on the first block (24 because of int truncating: each lockup gets 12.5 (== 12) two times) + // * every owner will get 24 stake more on the first block (total of 48 stake) + name: "Several blocks in the epoch, iteration weight = 4 (four lockup)", maxIterationsPerBlock: 5, numGauges: 16, - blocksInEpoch: 2, + blocksToProcess: 2, + initialLockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + {owner: addrs[2], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + {owner: addrs[3], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + }, streams: []types.Stream{ { Id: 1, DistrEpochIdentifier: "hour", - Coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100)), - DistributeTo: &types.DistrInfo{ + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ Records: []types.DistrRecord{ - {GaugeId: 1, Weight: math.NewInt(25)}, - {GaugeId: 2, Weight: math.NewInt(25)}, - {GaugeId: 3, Weight: math.NewInt(25)}, - {GaugeId: 4, Weight: math.NewInt(25)}, + {GaugeId: 1, Weight: math.NewInt(50)}, + {GaugeId: 2, Weight: math.NewInt(50)}, + {GaugeId: 3, Weight: math.NewInt(50)}, + {GaugeId: 4, Weight: math.NewInt(50)}, }, }, }, { Id: 2, DistrEpochIdentifier: "day", - Coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100)), - DistributeTo: &types.DistrInfo{ + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ Records: []types.DistrRecord{ - {GaugeId: 5, Weight: math.NewInt(25)}, - {GaugeId: 6, Weight: math.NewInt(25)}, - {GaugeId: 7, Weight: math.NewInt(25)}, - {GaugeId: 8, Weight: math.NewInt(25)}, + {GaugeId: 5, Weight: math.NewInt(50)}, + {GaugeId: 6, Weight: math.NewInt(50)}, + {GaugeId: 7, Weight: math.NewInt(50)}, + {GaugeId: 8, Weight: math.NewInt(50)}, }, }, }, { Id: 3, DistrEpochIdentifier: "day", - Coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100)), - DistributeTo: &types.DistrInfo{ + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ Records: []types.DistrRecord{ - {GaugeId: 9, Weight: math.NewInt(25)}, - {GaugeId: 10, Weight: math.NewInt(25)}, - {GaugeId: 11, Weight: math.NewInt(25)}, - {GaugeId: 12, Weight: math.NewInt(25)}, + {GaugeId: 9, Weight: math.NewInt(50)}, + {GaugeId: 10, Weight: math.NewInt(50)}, + {GaugeId: 11, Weight: math.NewInt(50)}, + {GaugeId: 12, Weight: math.NewInt(50)}, }, }, }, { Id: 4, DistrEpochIdentifier: "hour", - Coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100)), - DistributeTo: &types.DistrInfo{ + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ Records: []types.DistrRecord{ - {GaugeId: 13, Weight: math.NewInt(25)}, - {GaugeId: 14, Weight: math.NewInt(25)}, - {GaugeId: 15, Weight: math.NewInt(25)}, - {GaugeId: 16, Weight: math.NewInt(25)}, + {GaugeId: 13, Weight: math.NewInt(50)}, + {GaugeId: 14, Weight: math.NewInt(50)}, + {GaugeId: 15, Weight: math.NewInt(50)}, + {GaugeId: 16, Weight: math.NewInt(50)}, }, }, }, @@ -197,8 +1037,8 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { height: 0, epochPointers: []types.EpochPointer{ { - StreamId: 4, - GaugeId: 14, + StreamId: 1, + GaugeId: 3, EpochIdentifier: "hour", EpochDuration: time.Hour, }, @@ -211,23 +1051,23 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { // week epoch pointer is not used { StreamId: types.MinStreamID, - GaugeId: types.MinStreamID, + GaugeId: types.MinGaugeID, EpochIdentifier: "week", EpochDuration: 7 * 24 * time.Hour, }, }, distributedCoins: []distributedCoins{ - {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, {streamID: 2, coins: nil}, {streamID: 3, coins: nil}, - {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {streamID: 4, coins: nil}, }, gauges: []gaugeCoins{ // 1st stream - {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 48))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 48))}, + {gaugeID: 3, coins: nil}, + {gaugeID: 4, coins: nil}, // 2nd stream {gaugeID: 5, coins: nil}, {gaugeID: 6, coins: nil}, @@ -239,50 +1079,56 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { {gaugeID: 11, coins: nil}, {gaugeID: 12, coins: nil}, // 4th stream - {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 13, coins: nil}, {gaugeID: 14, coins: nil}, {gaugeID: 15, coins: nil}, {gaugeID: 16, coins: nil}, }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 24))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 24))}, + {owner: addrs[2], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 24))}, + {owner: addrs[3], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 24))}, + }, }, { height: 1, epochPointers: []types.EpochPointer{ { - StreamId: types.MaxStreamID, - GaugeId: types.MaxStreamID, + StreamId: 4, + GaugeId: 13, EpochIdentifier: "hour", EpochDuration: time.Hour, }, { - StreamId: 2, - GaugeId: 7, + StreamId: types.MinStreamID, + GaugeId: types.MinGaugeID, EpochIdentifier: "day", EpochDuration: 24 * time.Hour, }, // week epoch pointer is not used { StreamId: types.MinStreamID, - GaugeId: types.MinStreamID, + GaugeId: types.MinGaugeID, EpochIdentifier: "week", EpochDuration: 7 * 24 * time.Hour, }, }, distributedCoins: []distributedCoins{ - {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, - {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 50))}, + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: nil}, {streamID: 3, coins: nil}, - {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, + {streamID: 4, coins: nil}, }, gauges: []gaugeCoins{ // 1st stream - {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 48))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 48))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 48))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 48))}, // 2nd stream - {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 6, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 5, coins: nil}, + {gaugeID: 6, coins: nil}, {gaugeID: 7, coins: nil}, {gaugeID: 8, coins: nil}, // 3rd stream @@ -291,25 +1137,35 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { {gaugeID: 11, coins: nil}, {gaugeID: 12, coins: nil}, // 4th stream - {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 13, coins: nil}, + {gaugeID: 14, coins: nil}, + {gaugeID: 15, coins: nil}, + {gaugeID: 16, coins: nil}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 48))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 48))}, + {owner: addrs[2], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 48))}, + {owner: addrs[3], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 48))}, }, }, }, }, { - name: "Send all reward in one single block", - maxIterationsPerBlock: 5, + name: "Send all reward in one single block, iteration weight = 2 (two lockup)", + maxIterationsPerBlock: 100, numGauges: 4, - blocksInEpoch: 5, + blocksToProcess: 5, + initialLockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + }, streams: []types.Stream{ { Id: 1, DistrEpochIdentifier: "hour", - Coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 1)), - DistributeTo: &types.DistrInfo{ + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2)), + DistributeTo: types.DistrInfo{ Records: []types.DistrRecord{ {GaugeId: 1, Weight: math.NewInt(1)}, }, @@ -318,8 +1174,8 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { { Id: 2, DistrEpochIdentifier: "day", - Coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 1)), - DistributeTo: &types.DistrInfo{ + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2)), + DistributeTo: types.DistrInfo{ Records: []types.DistrRecord{ {GaugeId: 2, Weight: math.NewInt(1)}, }, @@ -328,8 +1184,8 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { { Id: 3, DistrEpochIdentifier: "day", - Coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 1)), - DistributeTo: &types.DistrInfo{ + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2)), + DistributeTo: types.DistrInfo{ Records: []types.DistrRecord{ {GaugeId: 3, Weight: math.NewInt(1)}, }, @@ -338,8 +1194,8 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { { Id: 4, DistrEpochIdentifier: "hour", - Coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 1)), - DistributeTo: &types.DistrInfo{ + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2)), + DistributeTo: types.DistrInfo{ Records: []types.DistrRecord{ {GaugeId: 4, Weight: math.NewInt(1)}, }, @@ -371,75 +1227,83 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { }, }, distributedCoins: []distributedCoins{ - {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 1))}, - {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 1))}, - {streamID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 1))}, - {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 1))}, + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2))}, + {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2))}, + {streamID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2))}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2))}, }, gauges: []gaugeCoins{ - {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 1))}, - {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 1))}, - {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 1))}, - {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 1))}, + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 2))}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 4))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 4))}, }, }, }, }, { - name: "Many blocks", + name: "Many blocks, iteration weight = 2 (two lockup)", maxIterationsPerBlock: 3, numGauges: 16, - blocksInEpoch: 100, + blocksToProcess: 200, + initialLockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + }, streams: []types.Stream{ { Id: 1, DistrEpochIdentifier: "hour", - Coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100)), - DistributeTo: &types.DistrInfo{ + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ Records: []types.DistrRecord{ - {GaugeId: 1, Weight: math.NewInt(25)}, - {GaugeId: 2, Weight: math.NewInt(25)}, - {GaugeId: 3, Weight: math.NewInt(25)}, - {GaugeId: 4, Weight: math.NewInt(25)}, + {GaugeId: 1, Weight: math.NewInt(50)}, + {GaugeId: 2, Weight: math.NewInt(50)}, + {GaugeId: 3, Weight: math.NewInt(50)}, + {GaugeId: 4, Weight: math.NewInt(50)}, }, }, }, { Id: 2, DistrEpochIdentifier: "day", - Coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100)), - DistributeTo: &types.DistrInfo{ + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ Records: []types.DistrRecord{ - {GaugeId: 5, Weight: math.NewInt(25)}, - {GaugeId: 6, Weight: math.NewInt(25)}, - {GaugeId: 7, Weight: math.NewInt(25)}, - {GaugeId: 8, Weight: math.NewInt(25)}, + {GaugeId: 5, Weight: math.NewInt(50)}, + {GaugeId: 6, Weight: math.NewInt(50)}, + {GaugeId: 7, Weight: math.NewInt(50)}, + {GaugeId: 8, Weight: math.NewInt(50)}, }, }, }, { Id: 3, DistrEpochIdentifier: "day", - Coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100)), - DistributeTo: &types.DistrInfo{ + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ Records: []types.DistrRecord{ - {GaugeId: 9, Weight: math.NewInt(25)}, - {GaugeId: 10, Weight: math.NewInt(25)}, - {GaugeId: 11, Weight: math.NewInt(25)}, - {GaugeId: 12, Weight: math.NewInt(25)}, + {GaugeId: 9, Weight: math.NewInt(50)}, + {GaugeId: 10, Weight: math.NewInt(50)}, + {GaugeId: 11, Weight: math.NewInt(50)}, + {GaugeId: 12, Weight: math.NewInt(50)}, }, }, }, { Id: 4, DistrEpochIdentifier: "hour", - Coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100)), - DistributeTo: &types.DistrInfo{ + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200)), + DistributeTo: types.DistrInfo{ Records: []types.DistrRecord{ - {GaugeId: 13, Weight: math.NewInt(25)}, - {GaugeId: 14, Weight: math.NewInt(25)}, - {GaugeId: 15, Weight: math.NewInt(25)}, - {GaugeId: 16, Weight: math.NewInt(25)}, + {GaugeId: 13, Weight: math.NewInt(50)}, + {GaugeId: 14, Weight: math.NewInt(50)}, + {GaugeId: 15, Weight: math.NewInt(50)}, + {GaugeId: 16, Weight: math.NewInt(50)}, }, }, }, @@ -450,7 +1314,7 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { epochPointers: []types.EpochPointer{ { StreamId: 1, - GaugeId: 4, + GaugeId: 3, EpochIdentifier: "hour", EpochDuration: time.Hour, }, @@ -463,22 +1327,22 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { // week epoch pointer is not used { StreamId: types.MinStreamID, - GaugeId: types.MinStreamID, + GaugeId: types.MinGaugeID, EpochIdentifier: "week", EpochDuration: 7 * 24 * time.Hour, }, }, distributedCoins: []distributedCoins{ - {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 75))}, + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, {streamID: 2, coins: nil}, {streamID: 3, coins: nil}, {streamID: 4, coins: nil}, }, gauges: []gaugeCoins{ // 1st stream - {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: nil}, {gaugeID: 4, coins: nil}, // 2nd stream {gaugeID: 5, coins: nil}, @@ -496,13 +1360,17 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { {gaugeID: 15, coins: nil}, {gaugeID: 16, coins: nil}, }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + }, }, { height: 1, epochPointers: []types.EpochPointer{ { StreamId: 4, - GaugeId: 15, + GaugeId: 13, EpochIdentifier: "hour", EpochDuration: time.Hour, }, @@ -515,23 +1383,23 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { // week epoch pointer is not used { StreamId: types.MinStreamID, - GaugeId: types.MinStreamID, + GaugeId: types.MinGaugeID, EpochIdentifier: "week", EpochDuration: 7 * 24 * time.Hour, }, }, distributedCoins: []distributedCoins{ - {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, {streamID: 2, coins: nil}, {streamID: 3, coins: nil}, - {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 50))}, + {streamID: 4, coins: nil}, }, gauges: []gaugeCoins{ // 1st stream - {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, // 2nd stream {gaugeID: 5, coins: nil}, {gaugeID: 6, coins: nil}, @@ -543,14 +1411,74 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { {gaugeID: 11, coins: nil}, {gaugeID: 12, coins: nil}, // 4th stream - {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 13, coins: nil}, + {gaugeID: 14, coins: nil}, {gaugeID: 15, coins: nil}, {gaugeID: 16, coins: nil}, }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + }, }, { height: 2, + epochPointers: []types.EpochPointer{ + { + StreamId: 4, + GaugeId: 15, + EpochIdentifier: "hour", + EpochDuration: time.Hour, + }, + { + StreamId: 0, + GaugeId: 0, + EpochIdentifier: "day", + EpochDuration: 24 * time.Hour, + }, + // week epoch pointer is not used + { + StreamId: types.MinStreamID, + GaugeId: types.MinGaugeID, + EpochIdentifier: "week", + EpochDuration: 7 * 24 * time.Hour, + }, + }, + distributedCoins: []distributedCoins{ + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: nil}, + {streamID: 3, coins: nil}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + }, + gauges: []gaugeCoins{ + // 1st stream + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 2nd stream + {gaugeID: 5, coins: nil}, + {gaugeID: 6, coins: nil}, + {gaugeID: 7, coins: nil}, + {gaugeID: 8, coins: nil}, + // 3rd stream + {gaugeID: 9, coins: nil}, + {gaugeID: 10, coins: nil}, + {gaugeID: 11, coins: nil}, + {gaugeID: 12, coins: nil}, + // 4th stream + {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 15, coins: nil}, + {gaugeID: 16, coins: nil}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 150))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 150))}, + }, + }, + { + height: 3, epochPointers: []types.EpochPointer{ { StreamId: types.MaxStreamID, @@ -559,33 +1487,33 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { EpochDuration: time.Hour, }, { - StreamId: 2, - GaugeId: 6, + StreamId: 0, + GaugeId: 0, EpochIdentifier: "day", EpochDuration: 24 * time.Hour, }, // week epoch pointer is not used { StreamId: types.MinStreamID, - GaugeId: types.MinStreamID, + GaugeId: types.MinGaugeID, EpochIdentifier: "week", EpochDuration: 7 * 24 * time.Hour, }, }, distributedCoins: []distributedCoins{ - {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, - {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: nil}, {streamID: 3, coins: nil}, - {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, }, gauges: []gaugeCoins{ // 1st stream - {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, // 2nd stream - {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 5, coins: nil}, {gaugeID: 6, coins: nil}, {gaugeID: 7, coins: nil}, {gaugeID: 8, coins: nil}, @@ -595,14 +1523,74 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { {gaugeID: 11, coins: nil}, {gaugeID: 12, coins: nil}, // 4th stream - {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, }, }, { height: 3, + epochPointers: []types.EpochPointer{ + { + StreamId: types.MaxStreamID, + GaugeId: types.MaxGaugeID, + EpochIdentifier: "hour", + EpochDuration: time.Hour, + }, + { + StreamId: 2, + GaugeId: 7, + EpochIdentifier: "day", + EpochDuration: 24 * time.Hour, + }, + // week epoch pointer is not used + { + StreamId: types.MinStreamID, + GaugeId: types.MinGaugeID, + EpochIdentifier: "week", + EpochDuration: 7 * 24 * time.Hour, + }, + }, + distributedCoins: []distributedCoins{ + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + {streamID: 3, coins: nil}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + }, + gauges: []gaugeCoins{ + // 1st stream + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 2nd stream + {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 6, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 7, coins: nil}, + {gaugeID: 8, coins: nil}, + // 3rd stream + {gaugeID: 9, coins: nil}, + {gaugeID: 10, coins: nil}, + {gaugeID: 11, coins: nil}, + {gaugeID: 12, coins: nil}, + // 4th stream + {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 250))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 250))}, + }, + }, + { + height: 4, epochPointers: []types.EpochPointer{ { StreamId: types.MaxStreamID, @@ -619,42 +1607,46 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { // week epoch pointer is not used { StreamId: types.MinStreamID, - GaugeId: types.MinStreamID, + GaugeId: types.MinGaugeID, EpochIdentifier: "week", EpochDuration: 7 * 24 * time.Hour, }, }, distributedCoins: []distributedCoins{ - {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, - {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, {streamID: 3, coins: nil}, - {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, }, gauges: []gaugeCoins{ // 1st stream - {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, // 2nd stream - {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 6, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 7, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 8, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 6, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 7, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 8, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, // 3rd stream {gaugeID: 9, coins: nil}, {gaugeID: 10, coins: nil}, {gaugeID: 11, coins: nil}, {gaugeID: 12, coins: nil}, // 4th stream - {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 300))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 300))}, }, }, { - height: 3, + height: 5, epochPointers: []types.EpochPointer{ { StreamId: types.MaxStreamID, @@ -664,49 +1656,53 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { }, { StreamId: 3, - GaugeId: 12, + GaugeId: 11, EpochIdentifier: "day", EpochDuration: 24 * time.Hour, }, // week epoch pointer is not used { StreamId: types.MinStreamID, - GaugeId: types.MinStreamID, + GaugeId: types.MinGaugeID, EpochIdentifier: "week", EpochDuration: 7 * 24 * time.Hour, }, }, distributedCoins: []distributedCoins{ - {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, - {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, - {streamID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 75))}, - {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 100))}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, }, gauges: []gaugeCoins{ // 1st stream - {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, // 2nd stream - {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 6, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 7, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 8, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 6, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 7, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 8, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, // 3rd stream - {gaugeID: 9, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 10, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 11, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 9, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 10, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 11, coins: nil}, {gaugeID: 12, coins: nil}, // 4th stream - {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 350))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 350))}, }, }, { - height: 4, + height: 6, epochPointers: []types.EpochPointer{ { StreamId: types.MaxStreamID, @@ -716,45 +1712,105 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { }, { StreamId: types.MaxStreamID, - GaugeId: types.MaxStreamID, + GaugeId: types.MaxGaugeID, EpochIdentifier: "day", EpochDuration: 24 * time.Hour, }, - // week epoch pointer is not used, however it points on the last gauge + // week epoch pointer is not used + { + StreamId: types.MinStreamID, + GaugeId: types.MinGaugeID, + EpochIdentifier: "week", + EpochDuration: 7 * 24 * time.Hour, + }, + }, + distributedCoins: []distributedCoins{ + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + }, + gauges: []gaugeCoins{ + // 1st stream + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 2nd stream + {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 6, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 7, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 8, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 3rd stream + {gaugeID: 9, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 10, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 11, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 12, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + // 4th stream + {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 400))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 400))}, + }, + }, + { + height: 7, + epochPointers: []types.EpochPointer{ { StreamId: types.MaxStreamID, - GaugeId: types.MaxStreamID, + GaugeId: types.MaxGaugeID, + EpochIdentifier: "hour", + EpochDuration: time.Hour, + }, + { + StreamId: types.MaxStreamID, + GaugeId: types.MaxGaugeID, + EpochIdentifier: "day", + EpochDuration: 24 * time.Hour, + }, + // week epoch pointer is not used + { + StreamId: types.MaxStreamID, + GaugeId: types.MaxGaugeID, EpochIdentifier: "week", EpochDuration: 7 * 24 * time.Hour, }, }, distributedCoins: []distributedCoins{ - {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, - {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, - {streamID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, - {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 100))}, + {streamID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, + {streamID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 200))}, }, gauges: []gaugeCoins{ // 1st stream - {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 1, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 2, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 3, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 4, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, // 2nd stream - {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 6, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 7, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 8, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 5, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 6, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 7, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 8, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, // 3rd stream - {gaugeID: 9, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 10, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 11, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 12, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 9, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 10, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 11, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 12, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, // 4th stream - {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, - {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("udym", 25))}, + {gaugeID: 13, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 14, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 15, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + {gaugeID: 16, coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 50))}, + }, + lockups: []lockup{ + {owner: addrs[0], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 400))}, + {owner: addrs[1], balance: sdk.NewCoins(sdk.NewInt64Coin("stake", 400))}, }, }, }, @@ -768,7 +1824,11 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { s.CreateGaugesUntil(tc.numGauges) - s.Require().LessOrEqual(len(tc.expectedBlockResults), tc.blocksInEpoch) + for _, lock := range tc.initialLockups { + s.LockTokens(lock.owner, lock.balance) + } + + s.Require().LessOrEqual(len(tc.expectedBlockResults), tc.blocksToProcess) // Update module params params := s.App.StreamerKeeper.GetParams(s.Ctx) @@ -785,7 +1845,7 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { err = s.App.StreamerKeeper.BeforeEpochStart(s.Ctx, "day") s.Require().NoError(err) - for i := range tc.blocksInEpoch { + for i := range tc.blocksToProcess { err = s.App.StreamerKeeper.EndBlock(s.Ctx) s.Require().NoError(err) @@ -793,7 +1853,7 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { gauges := s.App.IncentivesKeeper.GetGauges(s.Ctx) actualGauges := make(gaugeCoinsSlice, 0, len(gauges)) for _, gauge := range gauges { - actualGauges = append(actualGauges, gaugeCoins{gaugeID: gauge.Id, coins: gauge.Coins}) + actualGauges = append(actualGauges, gaugeCoins{gaugeID: gauge.Id, coins: gauge.DistributedCoins}) } // Check block results @@ -811,7 +1871,13 @@ func (s *KeeperTestSuite) TestProcessEpochPointer() { s.Require().Equal(expected.epochPointers, pointers) // Verify gauges are rewarded. Equality here is important! - s.Require().Equal(expected.gauges, actualGauges, "block height: %d\nexpect: %s\nactual: %s", i, expected, actualGauges) + s.Require().Equal(expected.gauges, actualGauges, "block height: %d\nexpect: %s\nactual: %s", i, expected.gauges, actualGauges) + + // Verify lockup owner are rewarded + for _, lock := range expected.lockups { + actualBalance := s.App.BankKeeper.GetAllBalances(s.Ctx, lock.owner) + s.Require().Equal(lock.balance, actualBalance) + } // Verify streams are valid active := s.App.StreamerKeeper.GetActiveStreams(s.Ctx) @@ -874,11 +1940,17 @@ func (s distributedCoinsSlice) String() string { return result } +type lockup struct { + owner sdk.AccAddress + balance sdk.Coins +} + type blockResults struct { height uint64 epochPointers []types.EpochPointer distributedCoins distributedCoinsSlice gauges gaugeCoinsSlice + lockups []lockup } func (b blockResults) String() string { diff --git a/x/streamer/keeper/distr_info.go b/x/streamer/keeper/distr_info.go index a415ecc0a..6ec5d376b 100644 --- a/x/streamer/keeper/distr_info.go +++ b/x/streamer/keeper/distr_info.go @@ -1,21 +1,21 @@ package keeper import ( - "github.com/dymensionxyz/dymension/v3/x/streamer/types" - errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/dymensionxyz/dymension/v3/x/streamer/types" ) -func (k Keeper) NewDistrInfo(ctx sdk.Context, records []types.DistrRecord) (*types.DistrInfo, error) { +func (k Keeper) NewDistrInfo(ctx sdk.Context, records []types.DistrRecord) (types.DistrInfo, error) { err := k.validateGauges(ctx, records) if err != nil { - return nil, err + return types.DistrInfo{}, err } distrInfo, err := types.NewDistrInfo(records) if err != nil { - return nil, err + return types.DistrInfo{}, err } return distrInfo, nil diff --git a/x/streamer/keeper/distr_info_test.go b/x/streamer/keeper/distr_info_test.go index 37d08c398..71b26191b 100644 --- a/x/streamer/keeper/distr_info_test.go +++ b/x/streamer/keeper/distr_info_test.go @@ -52,8 +52,6 @@ func (suite *KeeperTestSuite) TestAllocateToGauges() { for _, test := range tests { suite.Run(test.name, func() { - var streams []types.Stream - suite.CreateGauges(3) // create a stream @@ -61,9 +59,8 @@ func (suite *KeeperTestSuite) TestAllocateToGauges() { // move all created streams from upcoming to active suite.Ctx = suite.Ctx.WithBlockTime(time.Now()) - streams = suite.App.StreamerKeeper.GetStreams(suite.Ctx) - suite.DistributeAllRewards(streams) + suite.DistributeAllRewards() for i := 0; i < len(test.testingDistrRecord); i++ { if test.testingDistrRecord[i].GaugeId == 0 { diff --git a/x/streamer/keeper/distribute.go b/x/streamer/keeper/distribute.go index bf17a3557..0509cb689 100644 --- a/x/streamer/keeper/distribute.go +++ b/x/streamer/keeper/distribute.go @@ -5,110 +5,246 @@ import ( "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/osmosis-labs/osmosis/v15/osmoutils" - "github.com/dymensionxyz/dymension/v3/utils/pagination" + "github.com/dymensionxyz/dymension/v3/utils/cache" + incentivestypes "github.com/dymensionxyz/dymension/v3/x/incentives/types" "github.com/dymensionxyz/dymension/v3/x/streamer/types" ) -func (k Keeper) DistributeToGauge(ctx sdk.Context, coins sdk.Coins, record types.DistrRecord, totalWeight math.Int) (sdk.Coins, error) { +// Distribute distributes rewards to the provided streams within provided epochs considering the max number +// of iterations. +// It also sends coins to the x/incentives module before the gauge distribution and emits an end block event. +// The method uses three caches: +// - Stream cache for updating stream distributed coins +// - Gauge cache for updating gauge coins +// - Number of locks per denom to reduce the number of requests for x/lockup +// +// Returns distributed coins, the num of total iterations, and the error. +func (k Keeper) Distribute( + ctx sdk.Context, + epochPointers []types.EpochPointer, + streams []types.Stream, + maxOperations uint64, + epochEnd bool, +) (coins sdk.Coins, iterations uint64, err error) { + // Sort epoch pointers to distribute to shorter epochs first. Our goal is to fill streams with + // shorter epochs first. Otherwise, if a long stream has too many operations and takes entire blocks, + // then we will never start processing shorter streams during the epoch, and only process them at the epoch end. + types.SortEpochPointers(epochPointers) + + // Total operations counter. Each stream has some specific number of operations to process it. This counter + // serves as a meter for the number of operations that have been performed during stream processing. + // This is used to ensure that the method won't meet the upper bound for the complexity (maxOperations) + // that this method is capable of. + totalOperations := uint64(0) + totalDistributed := sdk.NewCoins() + + // Init helper caches + streamCache := cache.NewInsertionOrdered(types.Stream.Key, streams...) + gaugeCache := cache.NewInsertionOrdered(incentivestypes.Gauge.Key) + + // Cache specific for asset gauges. Helps reduce the number of x/lockup requests. + denomLockCache := incentivestypes.NewDenomLocksCache() + + for _, p := range epochPointers { + if totalOperations >= maxOperations { + // The upped bound of operations is met. No more operations available for this block. + break + } + + remainOperations := maxOperations - totalOperations // always positive + + // Calculate rewards and fill caches + distrCoins, newPointer, iters := k.CalculateRewards(ctx, p, remainOperations, streamCache, gaugeCache, denomLockCache) + + totalOperations += iters + totalDistributed = totalDistributed.Add(distrCoins...) + + err = k.SaveEpochPointer(ctx, newPointer) + if err != nil { + return nil, 0, fmt.Errorf("save epoch pointer: %w", err) + } + } + + // Send coins to distribute to the x/incentives module + if !totalDistributed.Empty() { + err = k.bk.SendCoinsFromModuleToModule(ctx, types.ModuleName, incentivestypes.ModuleName, totalDistributed) + if err != nil { + return nil, 0, fmt.Errorf("send coins: %w", err) + } + } + + // Distribute the rewards + _, err = k.ik.Distribute(ctx, gaugeCache.GetAll(), denomLockCache, epochEnd) + if err != nil { + return nil, 0, fmt.Errorf("distribute: %w", err) + } + + // Save stream updates + var rangeErr error + streamCache.Range(func(stream types.Stream) bool { + // If it is an epoch end, then update the stream info like the num of filled epochs + if epochEnd { + stream, rangeErr = k.UpdateStreamAtEpochEnd(ctx, stream) + if rangeErr != nil { + rangeErr = fmt.Errorf("update stream at epoch start: stream %d: %w", stream.Id, rangeErr) + return true + } + } + rangeErr = k.SetStream(ctx, &stream) + if rangeErr != nil { + rangeErr = fmt.Errorf("set stream: %w", rangeErr) + return true + } + return false + }) + if rangeErr != nil { + return nil, 0, rangeErr + } + + return totalDistributed, totalOperations, nil +} + +// CalculateGaugeRewards calculates the rewards to be distributed for a specific gauge based on the provided +// coins, distribution record, and total weight. The method iterates through the coins, calculates the +// allocating amount based on the weight of the gauge and the total weight, and adds the allocated amount +// to the rewards. If the allocating amount is not positive, the coin is skipped. +func (k Keeper) CalculateGaugeRewards(ctx sdk.Context, coins sdk.Coins, record types.DistrRecord, totalWeight math.Int) (sdk.Coins, error) { if coins.Empty() { - return sdk.Coins{}, fmt.Errorf("coins to allocate cannot be empty") + return nil, fmt.Errorf("coins to allocate cannot be empty") } if totalWeight.IsZero() { - return sdk.Coins{}, fmt.Errorf("distribution total weight cannot be zero") + return nil, fmt.Errorf("distribution total weight cannot be zero") } - totalAllocated := sdk.NewCoins() + weightDec := sdk.NewDecFromInt(record.Weight) + totalDec := sdk.NewDecFromInt(totalWeight) + rewards := sdk.NewCoins() + for _, coin := range coins { if coin.IsZero() { continue } assetAmountDec := sdk.NewDecFromInt(coin.Amount) - weightDec := sdk.NewDecFromInt(record.Weight) - totalDec := sdk.NewDecFromInt(totalWeight) allocatingAmount := assetAmountDec.Mul(weightDec.Quo(totalDec)).TruncateInt() // when weight is too small and no amount is allocated, just skip this to avoid zero coin send issues if !allocatingAmount.IsPositive() { - k.Logger(ctx).Info(fmt.Sprintf("allocating amount for (%d, %s) record is not positive", record.GaugeId, record.Weight.String())) + k.Logger(ctx).Info(fmt.Sprintf("allocating amount for gauge is not positive: gauge '%d', weight %s", record.GaugeId, record.Weight.String())) continue } - _, err := k.ik.GetGaugeByID(ctx, record.GaugeId) - if err != nil { - return sdk.Coins{}, fmt.Errorf("get gauge %d: %w", record.GaugeId, err) - } - allocatedCoin := sdk.Coin{Denom: coin.Denom, Amount: allocatingAmount} - err = k.ik.AddToGaugeRewardsByID(ctx, k.ak.GetModuleAddress(types.ModuleName), sdk.NewCoins(allocatedCoin), record.GaugeId) - if err != nil { - return sdk.Coins{}, fmt.Errorf("add rewards to gauge %d: %w", record.GaugeId, err) - } - - totalAllocated = totalAllocated.Add(allocatedCoin) + rewards = rewards.Add(allocatedCoin) } - return totalAllocated, nil -} - -type DistributeRewardsResult struct { - NewPointer types.EpochPointer - FilledStreams []types.Stream - DistributedCoins sdk.Coins - Iterations uint64 + return rewards, nil } -// DistributeRewards distributes all streams rewards to the corresponding gauges starting with -// the specified pointer and considering the limit. -func (k Keeper) DistributeRewards( +// CalculateRewards calculates rewards for streams and corresponding gauges. Is starts processing gauges from +// the specified pointer and considering the limit. This method doesn't have any state updates, it only +// calculates rewards and fills respective caches. +// Returns a new pointer, total distr coins, and the num of iterations. +func (k Keeper) CalculateRewards( ctx sdk.Context, pointer types.EpochPointer, limit uint64, - streams []types.Stream, -) DistributeRewardsResult { - totalDistributed := sdk.NewCoins() - - // Temporary map for convenient calculations - streamUpdates := make(map[uint64]sdk.Coins, len(streams)) - - // Distribute to all the remaining gauges that are left after EndBlock - newPointer, iterations := IterateEpochPointer(pointer, streams, limit, func(v StreamGauge) pagination.Stop { - var distributed sdk.Coins - err := osmoutils.ApplyFuncIfNoError(ctx, func(ctx sdk.Context) error { + streamCache *cache.InsertionOrdered[uint64, types.Stream], + gaugeCache *cache.InsertionOrdered[uint64, incentivestypes.Gauge], + denomLocksCache incentivestypes.DenomLocksCache, +) (distributedCoins sdk.Coins, newPointer types.EpochPointer, operations uint64) { + distributedCoins = sdk.NewCoins() + pointer, operations = IterateEpochPointer(pointer, streamCache.GetAll(), limit, func(v StreamGauge) (stop bool, operations uint64) { + // get stream from the cache since we need to use the last updated version + stream := streamCache.MustGet(v.Stream.Id) + + // get gauge from the cache since we need to use the last updated version + gauge, ok := gaugeCache.Get(v.Gauge.GaugeId) + if !ok { + // If the gauge is not found, then + // 1. Request it from the incentives keeper and validate the gauge exists + // 2. Validate that it's not finished + // 3. If everything is fine, then add the gauge to the cache, and use the cached versions in the future var err error - distributed, err = k.DistributeToGauge(ctx, v.Stream.EpochCoins, v.Gauge, v.Stream.DistributeTo.TotalWeight) - return err - }) + gauge, err = k.getActiveGaugeByID(ctx, v.Gauge.GaugeId) + if err != nil { + // we don't want to fail in this case, ignore this gauge + k.Logger(ctx). + With("gaugeID", v.Gauge.GaugeId, "error", err.Error()). + Error("Can't distribute to gauge: failed to get active gauge") + return false, 0 // continue, weight = 0, consider this operation as it is free + } + // add a new gauge to the cache + gaugeCache.Upsert(gauge) + } + + rewards, err := k.CalculateGaugeRewards( + ctx, + v.Stream.EpochCoins, + v.Gauge, + stream.DistributeTo.TotalWeight, + ) if err != nil { - // Ignore this gauge + // we don't want to fail in this case, ignore this gauge k.Logger(ctx). - With("streamID", v.Stream.Id, "gaugeID", v.Gauge.GaugeId, "error", err.Error()). + With("streamID", stream.Id, "gaugeID", v.Gauge.GaugeId, "error", err.Error()). Error("Failed to distribute to gauge") - return pagination.Continue + return false, 0 // continue, weight = 0, consider this operation as it is free } - totalDistributed = totalDistributed.Add(distributed...) + // update distributed coins for the stream + stream.AddDistributedCoins(rewards) + streamCache.Upsert(stream) + + // update distributed coins for the gauge + gauge.AddCoins(rewards) + gaugeCache.Upsert(gauge) + + // get gauge weight and update denomLocksCache under the hood + operations = k.getGaugeLockNum(ctx, gauge, denomLocksCache) - // Update distributed coins for the stream - update := streamUpdates[v.Stream.Id] - update = update.Add(distributed...) - streamUpdates[v.Stream.Id] = update + distributedCoins = distributedCoins.Add(rewards...) - return pagination.Continue + return false, operations }) + return distributedCoins, pointer, operations +} - for i, s := range streams { - s.DistributedCoins = s.DistributedCoins.Add(streamUpdates[s.Id]...) - streams[i] = s +// getActiveGaugeByID returns the active gauge with the given ID from the keeper. +// An error is returned if the gauge does not exist or if it is finished. +func (k Keeper) getActiveGaugeByID(ctx sdk.Context, gaugeID uint64) (incentivestypes.Gauge, error) { + // validate the gauge exists + gauge, err := k.ik.GetGaugeByID(ctx, gaugeID) + if err != nil { + return incentivestypes.Gauge{}, fmt.Errorf("get gauge: id %d: %w", gaugeID, err) + } + // validate the gauge is not finished + finished := gauge.IsFinishedGauge(ctx.BlockTime()) + if finished { + return incentivestypes.Gauge{}, incentivestypes.UnexpectedFinishedGaugeError{GaugeId: gaugeID} } + return *gauge, nil +} - return DistributeRewardsResult{ - NewPointer: newPointer, - FilledStreams: streams, // Make sure that the returning slice is always sorted - DistributedCoins: totalDistributed, - Iterations: iterations, +// getGaugeLockNum returns the number of locks for the specified gauge. +// If the gauge is an asset gauge, return the number of associated lockups. Use cache to get this figure. +// If the gauge is a rollapp gauge, the number is always 1. Imagine a rollapp as a single lockup. +// If the gauge has an unknown type, the weight is 0 as we assume that the gauge is always validated at this step +// and has a known type. The method also fills the cache under the hood. +func (k Keeper) getGaugeLockNum(ctx sdk.Context, gauge incentivestypes.Gauge, cache incentivestypes.DenomLocksCache) uint64 { + switch gauge.DistributeTo.(type) { + case *incentivestypes.Gauge_Asset: + // GetDistributeToBaseLocks fills the cache + locks := k.ik.GetDistributeToBaseLocks(ctx, gauge, cache) + // asset gauge weight is the num of associated lockups + return uint64(len(locks)) + case *incentivestypes.Gauge_Rollapp: + // rollapp gauge weight is always 1 + return 1 + default: + // assume that the gauge is always validated at this step and has a known type + return 0 } } diff --git a/x/streamer/keeper/distribute_test.go b/x/streamer/keeper/distribute_test.go index 64986d932..6e3cafaac 100644 --- a/x/streamer/keeper/distribute_test.go +++ b/x/streamer/keeper/distribute_test.go @@ -4,13 +4,12 @@ import ( "time" "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/suite" "github.com/dymensionxyz/dymension/v3/app/apptesting" sponsorshiptypes "github.com/dymensionxyz/dymension/v3/x/sponsorship/types" "github.com/dymensionxyz/dymension/v3/x/streamer/types" - - sdk "github.com/cosmos/cosmos-sdk/types" ) var _ = suite.TestingSuite(nil) @@ -59,14 +58,11 @@ func (suite *KeeperTestSuite) TestDistribute() { suite.SetupTest() // Setup streams and defined in the above tests, then distribute to them - var streams []types.Stream gaugesExpectedRewards := make(map[uint64]sdk.Coins) for _, stream := range tc.streams { // Create a stream, move it from upcoming to active and update its parameters _, newStream := suite.CreateStream(stream.distrInfo, stream.coins, time.Now().Add(-time.Minute), "day", stream.numOfEpochs) - streams = append(streams, *newStream) - // Calculate expected rewards for _, coin := range stream.coins { epochAmt := coin.Amount.Quo(sdk.NewInt(int64(stream.numOfEpochs))) @@ -82,7 +78,7 @@ func (suite *KeeperTestSuite) TestDistribute() { } // Trigger the distribution - suite.DistributeAllRewards(streams) + suite.DistributeAllRewards() // Check expected rewards against actual rewards received gauges := suite.App.IncentivesKeeper.GetGauges(suite.Ctx) @@ -234,6 +230,10 @@ func (suite *KeeperTestSuite) TestSponsoredDistribute() { suite.Run(tc.name, func() { suite.SetupTest() + // We must create at least one lock, otherwise distribution won't work + lockOwner := apptesting.CreateRandomAccounts(1)[0] + suite.LockTokens(lockOwner, sdk.NewCoins(sdk.NewInt64Coin("stake", 100))) + // Cast an initial vote if tc.hasInitialDistr { suite.Vote(tc.initialVote, sponsorshiptypes.DYM) @@ -336,10 +336,9 @@ func (suite *KeeperTestSuite) TestGetModuleToDistributeCoins() { // move all created streams from upcoming to active suite.Ctx = suite.Ctx.WithBlockTime(time.Now()) - streams := suite.App.StreamerKeeper.GetStreams(suite.Ctx) // distribute coins to stakers - distrCoins := suite.DistributeAllRewards(streams) + distrCoins := suite.DistributeAllRewards() suite.Require().NoError(err) suite.Require().Equal(sdk.Coins{sdk.NewInt64Coin("stake", 20000), sdk.NewInt64Coin("udym", 10000)}, distrCoins) diff --git a/x/streamer/keeper/distributed_coins.go b/x/streamer/keeper/distributed_coins.go index d9b91df67..fe93b2cde 100644 --- a/x/streamer/keeper/distributed_coins.go +++ b/x/streamer/keeper/distributed_coins.go @@ -3,6 +3,7 @@ package keeper import ( db "github.com/cometbft/cometbft-db" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/dymensionxyz/dymension/v3/x/streamer/types" ) diff --git a/x/streamer/keeper/genesis_test.go b/x/streamer/keeper/genesis_test.go index ab515e2d5..0371ded50 100644 --- a/x/streamer/keeper/genesis_test.go +++ b/x/streamer/keeper/genesis_test.go @@ -107,7 +107,7 @@ func TestStreamerInitGenesis(t *testing.T) { stream := types.Stream{ Id: 1, - DistributeTo: &distr, + DistributeTo: distr, Coins: coins, NumEpochsPaidOver: 2, DistrEpochIdentifier: "day", @@ -167,7 +167,7 @@ func TestStreamerOrder(t *testing.T) { stream := types.Stream{ Id: 1, - DistributeTo: &distr, + DistributeTo: distr, Coins: coins, NumEpochsPaidOver: 2, DistrEpochIdentifier: "day", @@ -179,7 +179,7 @@ func TestStreamerOrder(t *testing.T) { stream2 := types.Stream{ Id: 2, - DistributeTo: &distr, + DistributeTo: distr, Coins: coins, NumEpochsPaidOver: 2, DistrEpochIdentifier: "day", @@ -191,7 +191,7 @@ func TestStreamerOrder(t *testing.T) { stream3 := types.Stream{ Id: 3, - DistributeTo: &distr, + DistributeTo: distr, Coins: coins, NumEpochsPaidOver: 2, DistrEpochIdentifier: "day", diff --git a/x/streamer/keeper/grpc_query.go b/x/streamer/keeper/grpc_query.go index a3ad0f2a2..f3ec9666b 100644 --- a/x/streamer/keeper/grpc_query.go +++ b/x/streamer/keeper/grpc_query.go @@ -4,12 +4,11 @@ import ( "context" "encoding/json" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/query" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "github.com/dymensionxyz/dymension/v3/x/streamer/types" ) diff --git a/x/streamer/keeper/grpc_query_test.go b/x/streamer/keeper/grpc_query_test.go index 8b07ef369..ed099a07d 100644 --- a/x/streamer/keeper/grpc_query_test.go +++ b/x/streamer/keeper/grpc_query_test.go @@ -4,10 +4,9 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" "github.com/stretchr/testify/suite" - query "github.com/cosmos/cosmos-sdk/types/query" - "github.com/dymensionxyz/dymension/v3/x/streamer/types" ) diff --git a/x/streamer/keeper/hooks.go b/x/streamer/keeper/hooks.go index 2c1fd4667..0787fe974 100644 --- a/x/streamer/keeper/hooks.go +++ b/x/streamer/keeper/hooks.go @@ -58,7 +58,7 @@ func (k Keeper) BeforeEpochStart(ctx sdk.Context, epochIdentifier string) error for _, s := range toStart { updated, err := k.UpdateStreamAtEpochStart(ctx, s) if err != nil { - return fmt.Errorf("update stream '%d' at epoch start: %w", s.Id, err) + return fmt.Errorf("update stream at epoch start: stream %d: %w", s.Id, err) } // Save the stream err = k.SetStream(ctx, &updated) @@ -81,58 +81,51 @@ func (k Keeper) BeforeEpochStart(ctx sdk.Context, epochIdentifier string) error // It distributes rewards to streams that have the specified epoch identifier or aborts if there are no streams // in this epoch. After the distribution, it resets the epoch pointer to the very fist gauge. func (k Keeper) AfterEpochEnd(ctx sdk.Context, epochIdentifier string) (sdk.Coins, error) { - toDistribute := k.GetActiveStreamsForEpoch(ctx, epochIdentifier) - - if len(toDistribute) == 0 { + // Get active streams + activeStreams := k.GetActiveStreamsForEpoch(ctx, epochIdentifier) + if len(activeStreams) == 0 { // Nothing to distribute return sdk.Coins{}, nil } + // Get epoch pointer for the current epoch epochPointer, err := k.GetEpochPointer(ctx, epochIdentifier) if err != nil { - return sdk.Coins{}, fmt.Errorf("get epoch pointer for epoch '%s': %w", epochIdentifier, err) + return sdk.Coins{}, fmt.Errorf("get epoch pointer: epoch '%s': %w", epochIdentifier, err) } - distrResult := k.DistributeRewards(ctx, epochPointer, types.IterationsNoLimit, toDistribute) - - // Update streams with respect to a new epoch and save them - for _, s := range distrResult.FilledStreams { - updated, err := k.UpdateStreamAtEpochEnd(ctx, s) - if err != nil { - return sdk.Coins{}, fmt.Errorf("update stream '%d' at epoch start: %w", s.Id, err) - } - // Save the stream - err = k.SetStream(ctx, &updated) - if err != nil { - return sdk.Coins{}, fmt.Errorf("set stream: %w", err) - } + // Distribute rewards + const epochEnd = true + coins, iterations, err := k.Distribute(ctx, []types.EpochPointer{epochPointer}, activeStreams, types.IterationsNoLimit, epochEnd) + if err != nil { + return sdk.Coins{}, fmt.Errorf("distribute: %w", err) } // Reset the epoch pointer - distrResult.NewPointer.SetToFirstGauge() - err = k.SaveEpochPointer(ctx, distrResult.NewPointer) + epochPointer.SetToFirstGauge() + err = k.SaveEpochPointer(ctx, epochPointer) if err != nil { return sdk.Coins{}, fmt.Errorf("save epoch pointer: %w", err) } - err = ctx.EventManager().EmitTypedEvent(&types.EventEpochEnd{ - Iterations: distrResult.Iterations, - Distributed: distrResult.DistributedCoins, + err = uevent.EmitTypedEvent(ctx, &types.EventEpochEnd{ + Iterations: iterations, + Distributed: coins, }) if err != nil { return sdk.Coins{}, fmt.Errorf("emit typed event: %w", err) } - ctx.Logger().Info("Streamer distributed coins", "amount", distrResult.DistributedCoins.String()) + ctx.Logger().Info("Streamer distributed coins", "amount", coins.String()) - return distrResult.DistributedCoins, nil + return coins, nil } // BeforeEpochStart is the epoch start hook. func (h Hooks) BeforeEpochStart(ctx sdk.Context, epochIdentifier string, _ int64) error { err := h.k.BeforeEpochStart(ctx, epochIdentifier) if err != nil { - return fmt.Errorf("x/streamer: before epoch '%s' start: %w", epochIdentifier, err) + return fmt.Errorf("x/streamer: before epoch start: epoch '%s': %w", epochIdentifier, err) } return nil } @@ -141,7 +134,7 @@ func (h Hooks) BeforeEpochStart(ctx sdk.Context, epochIdentifier string, _ int64 func (h Hooks) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, _ int64) error { _, err := h.k.AfterEpochEnd(ctx, epochIdentifier) if err != nil { - return fmt.Errorf("x/streamer: after epoch '%s' end: %w", epochIdentifier, err) + return fmt.Errorf("x/streamer: epoch end: epoch '%s': %w", epochIdentifier, err) } return nil } diff --git a/x/streamer/keeper/hooks_test.go b/x/streamer/keeper/hooks_test.go index 6a78957d4..af3e27be2 100644 --- a/x/streamer/keeper/hooks_test.go +++ b/x/streamer/keeper/hooks_test.go @@ -4,11 +4,11 @@ import ( "time" "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/suite" + "github.com/dymensionxyz/dymension/v3/app/apptesting" "github.com/dymensionxyz/dymension/v3/x/streamer/types" - - sdk "github.com/cosmos/cosmos-sdk/types" ) var _ = suite.TestingSuite(nil) @@ -24,6 +24,10 @@ func (suite *KeeperTestSuite) TestHookOperation() { err := suite.CreateGauge() suite.Require().NoError(err) + // we must create at least one lock, otherwise distribution won't work + lockOwner := apptesting.CreateRandomAccounts(1)[0] + suite.LockTokens(lockOwner, sdk.NewCoins(sdk.NewInt64Coin("stake", 100))) + // initial module streams check streams := suite.App.StreamerKeeper.GetNotFinishedStreams(suite.Ctx) suite.Require().Len(streams, 0) diff --git a/x/streamer/keeper/invariants.go b/x/streamer/keeper/invariants.go index f7f5f91c8..312788c5f 100644 --- a/x/streamer/keeper/invariants.go +++ b/x/streamer/keeper/invariants.go @@ -4,6 +4,7 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/dymensionxyz/dymension/v3/x/streamer/types" ) diff --git a/x/streamer/keeper/iterator.go b/x/streamer/keeper/iterator.go index e0ee9c8bc..c7ee959f3 100644 --- a/x/streamer/keeper/iterator.go +++ b/x/streamer/keeper/iterator.go @@ -4,9 +4,9 @@ import ( "encoding/json" db "github.com/cometbft/cometbft-db" - "github.com/dymensionxyz/dymension/v3/x/streamer/types" - sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/dymensionxyz/dymension/v3/x/streamer/types" ) // iterator returns an iterator over all streams in the {prefix} space of state. diff --git a/x/streamer/keeper/keeper.go b/x/streamer/keeper/keeper.go index faf4978d3..539043683 100644 --- a/x/streamer/keeper/keeper.go +++ b/x/streamer/keeper/keeper.go @@ -7,16 +7,15 @@ import ( "cosmossdk.io/collections" "github.com/cometbft/cometbft/libs/log" "github.com/cosmos/cosmos-sdk/codec" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" "github.com/osmosis-labs/osmosis/v15/osmoutils" epochstypes "github.com/osmosis-labs/osmosis/v15/x/epochs/types" "github.com/dymensionxyz/dymension/v3/internal/collcompat" "github.com/dymensionxyz/dymension/v3/x/streamer/types" - - storetypes "github.com/cosmos/cosmos-sdk/store/types" - sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" ) // Keeper provides a way to manage streamer module storage. @@ -79,7 +78,7 @@ func (k Keeper) CreateStream(ctx sdk.Context, coins sdk.Coins, records []types.D return 0, fmt.Errorf("all coins %s must be positive", coins) } - var distrInfo *types.DistrInfo + var distrInfo types.DistrInfo if sponsored { distr, err := k.sk.GetDistribution(ctx) if err != nil { diff --git a/x/streamer/keeper/keeper_replace_update_distribution_test.go b/x/streamer/keeper/keeper_replace_update_distribution_test.go index eb37d798d..92846150d 100644 --- a/x/streamer/keeper/keeper_replace_update_distribution_test.go +++ b/x/streamer/keeper/keeper_replace_update_distribution_test.go @@ -2,6 +2,7 @@ package keeper_test import ( sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/dymensionxyz/dymension/v3/x/streamer/types" ) diff --git a/x/streamer/keeper/params.go b/x/streamer/keeper/params.go index 819a00f8e..08aa44d04 100644 --- a/x/streamer/keeper/params.go +++ b/x/streamer/keeper/params.go @@ -1,9 +1,9 @@ package keeper import ( - "github.com/dymensionxyz/dymension/v3/x/streamer/types" - sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/dymensionxyz/dymension/v3/x/streamer/types" ) // GetParams returns all of the parameters in the incentive module. diff --git a/x/streamer/keeper/stream_iterator.go b/x/streamer/keeper/stream_iterator.go index ce8ff28fc..5f02a8605 100644 --- a/x/streamer/keeper/stream_iterator.go +++ b/x/streamer/keeper/stream_iterator.go @@ -11,7 +11,7 @@ func IterateEpochPointer( p types.EpochPointer, streams []types.Stream, maxIterations uint64, - cb func(v StreamGauge) pagination.Stop, + cb func(v StreamGauge) (stop bool, weight uint64), ) (types.EpochPointer, uint64) { iter := NewStreamIterator(streams, p.StreamId, p.GaugeId, p.EpochIdentifier) iterations := pagination.Paginate(iter, maxIterations, cb) diff --git a/x/streamer/keeper/stream_iterator_test.go b/x/streamer/keeper/stream_iterator_test.go index 5d8dee748..608c652ea 100644 --- a/x/streamer/keeper/stream_iterator_test.go +++ b/x/streamer/keeper/stream_iterator_test.go @@ -5,7 +5,6 @@ import ( "github.com/stretchr/testify/require" - "github.com/dymensionxyz/dymension/v3/utils/pagination" "github.com/dymensionxyz/dymension/v3/x/streamer/keeper" "github.com/dymensionxyz/dymension/v3/x/streamer/types" ) @@ -21,7 +20,7 @@ func TestStreamIterator(t *testing.T) { } return types.Stream{ Id: id, - DistributeTo: &types.DistrInfo{Records: g}, + DistributeTo: types.DistrInfo{Records: g}, DistrEpochIdentifier: epochID, } } @@ -1292,9 +1291,9 @@ func TestStreamIterator(t *testing.T) { t.Parallel() var traversal [][2]uint64 - newPointer, iters := keeper.IterateEpochPointer(tc.pointer, tc.streams, tc.maxIters, func(v keeper.StreamGauge) pagination.Stop { + newPointer, iters := keeper.IterateEpochPointer(tc.pointer, tc.streams, tc.maxIters, func(v keeper.StreamGauge) (stop bool, weight uint64) { traversal = append(traversal, [2]uint64{v.Stream.Id, v.Gauge.GaugeId}) - return pagination.Continue + return false, 1 }) require.Equal(t, tc.expectedIters, iters) diff --git a/x/streamer/keeper/suite_test.go b/x/streamer/keeper/suite_test.go index 0a93fcc02..595390bf9 100644 --- a/x/streamer/keeper/suite_test.go +++ b/x/streamer/keeper/suite_test.go @@ -1,7 +1,6 @@ package keeper_test import ( - "slices" "testing" "time" @@ -67,7 +66,7 @@ func (suite *KeeperTestSuite) CreateGauge() error { suite.App.AccountKeeper.GetModuleAddress(types.ModuleName), sdk.Coins{}, lockuptypes.QueryCondition{ - LockQueryType: lockuptypes.ByTime, + LockQueryType: lockuptypes.ByDuration, Denom: "stake", Duration: time.Hour, Timestamp: time.Time{}, @@ -227,21 +226,21 @@ func (suite *KeeperTestSuite) Delegate(delAddr sdk.AccAddress, valAddr sdk.ValAd return del } -func (suite *KeeperTestSuite) DistributeAllRewards(streams []types.Stream) sdk.Coins { - rewards := sdk.Coins{} - suite.Require().True(slices.IsSortedFunc(streams, keeper.CmpStreams)) - for _, stream := range streams { - epoch := suite.App.EpochsKeeper.GetEpochInfo(suite.Ctx, stream.DistrEpochIdentifier) - res := suite.App.StreamerKeeper.DistributeRewards( - suite.Ctx, - types.NewEpochPointer(epoch.Identifier, epoch.Duration), - types.IterationsNoLimit, - []types.Stream{stream}, - ) - suite.Require().Len(res.FilledStreams, 1) - err := suite.App.StreamerKeeper.SetStream(suite.Ctx, &res.FilledStreams[0]) - suite.Require().NoError(err) - rewards = rewards.Add(res.DistributedCoins...) - } - return rewards +func (suite *KeeperTestSuite) DistributeAllRewards() sdk.Coins { + // We must create at least one lock, otherwise distribution won't work + lockOwner := apptesting.CreateRandomAccounts(1)[0] + suite.LockTokens(lockOwner, sdk.NewCoins(sdk.NewInt64Coin("stake", 100))) + + err := suite.App.StreamerKeeper.BeforeEpochStart(suite.Ctx, "day") + suite.Require().NoError(err) + coins, err := suite.App.StreamerKeeper.AfterEpochEnd(suite.Ctx, "day") + suite.Require().NoError(err) + return coins +} + +// LockTokens locks tokens for the specified duration +func (suite *KeeperTestSuite) LockTokens(addr sdk.AccAddress, coins sdk.Coins) { + suite.FundAcc(addr, coins) + _, err := suite.App.LockupKeeper.CreateLock(suite.Ctx, addr, coins, time.Hour) + suite.Require().NoError(err) } diff --git a/x/streamer/keeper/utils.go b/x/streamer/keeper/utils.go index 09998831f..85a677bdf 100644 --- a/x/streamer/keeper/utils.go +++ b/x/streamer/keeper/utils.go @@ -3,9 +3,9 @@ package keeper import ( "time" - "github.com/dymensionxyz/dymension/v3/x/streamer/types" - sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/dymensionxyz/dymension/v3/x/streamer/types" ) // findIndex takes an array of IDs. Then return the index of a specific ID. diff --git a/x/streamer/keeper/utils_test.go b/x/streamer/keeper/utils_test.go index 4150c03e9..96376f0f4 100644 --- a/x/streamer/keeper/utils_test.go +++ b/x/streamer/keeper/utils_test.go @@ -5,11 +5,10 @@ import ( "testing" "time" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" "github.com/dymensionxyz/dymension/v3/x/streamer/types" - - sdk "github.com/cosmos/cosmos-sdk/types" ) func TestCombineKeys(t *testing.T) { diff --git a/x/streamer/types/distr_info.go b/x/streamer/types/distr_info.go index 76d43b503..d183c926c 100644 --- a/x/streamer/types/distr_info.go +++ b/x/streamer/types/distr_info.go @@ -7,13 +7,13 @@ import ( sponsorshiptypes "github.com/dymensionxyz/dymension/v3/x/sponsorship/types" ) -func NewDistrInfo(records []DistrRecord) (*DistrInfo, error) { +func NewDistrInfo(records []DistrRecord) (DistrInfo, error) { distrInfo := DistrInfo{} totalWeight := sdk.NewInt(0) for _, record := range records { if err := record.ValidateBasic(); err != nil { - return nil, err + return DistrInfo{}, err } totalWeight = totalWeight.Add(record.Weight) } @@ -22,10 +22,10 @@ func NewDistrInfo(records []DistrRecord) (*DistrInfo, error) { distrInfo.TotalWeight = totalWeight if !totalWeight.IsPositive() { - return nil, ErrDistrInfoNotPositiveWeight + return DistrInfo{}, ErrDistrInfoNotPositiveWeight } - return &distrInfo, nil + return distrInfo, nil } // ValidateBasic is a basic validation test on recordd distribution gauges' weights. @@ -54,7 +54,7 @@ func (r DistrRecord) ValidateBasic() error { // Gauge2: 50% / 80% = 62.5% (in the new distribution) // // So, Gauge1 gets 37.5 DYM, and Gauge2 gets 62.5 DYM. -func DistrInfoFromDistribution(d sponsorshiptypes.Distribution) *DistrInfo { +func DistrInfoFromDistribution(d sponsorshiptypes.Distribution) DistrInfo { totalWeight := math.ZeroInt() records := make([]DistrRecord, 0, len(d.Gauges)) @@ -66,7 +66,7 @@ func DistrInfoFromDistribution(d sponsorshiptypes.Distribution) *DistrInfo { totalWeight = totalWeight.Add(g.Power) } - return &DistrInfo{ + return DistrInfo{ TotalWeight: totalWeight, Records: records, } diff --git a/x/streamer/types/distr_info_test.go b/x/streamer/types/distr_info_test.go index 28b805186..34a9bd4d2 100644 --- a/x/streamer/types/distr_info_test.go +++ b/x/streamer/types/distr_info_test.go @@ -14,12 +14,12 @@ func TestDistrInfoFromDistribution(t *testing.T) { testCases := []struct { name string distr sponsorshiptypes.Distribution - expDistr *types.DistrInfo + expDistr types.DistrInfo }{ { name: "Empty distribution", distr: sponsorshiptypes.NewDistribution(), - expDistr: &types.DistrInfo{ + expDistr: types.DistrInfo{ TotalWeight: sdk.NewInt(0), Records: []types.DistrRecord{}, }, @@ -35,7 +35,7 @@ func TestDistrInfoFromDistribution(t *testing.T) { }, }, }, - expDistr: &types.DistrInfo{ + expDistr: types.DistrInfo{ TotalWeight: sdk.NewInt(10), Records: []types.DistrRecord{ { @@ -60,7 +60,7 @@ func TestDistrInfoFromDistribution(t *testing.T) { }, }, }, - expDistr: &types.DistrInfo{ + expDistr: types.DistrInfo{ TotalWeight: sdk.NewInt(30), Records: []types.DistrRecord{ { @@ -80,7 +80,7 @@ func TestDistrInfoFromDistribution(t *testing.T) { VotingPower: sdk.NewInt(30), Gauges: []sponsorshiptypes.Gauge{}, }, - expDistr: &types.DistrInfo{ + expDistr: types.DistrInfo{ TotalWeight: sdk.ZeroInt(), Records: []types.DistrRecord{}, }, @@ -101,7 +101,7 @@ func TestDistrInfoFromDistribution(t *testing.T) { }, }, }, - expDistr: &types.DistrInfo{ + expDistr: types.DistrInfo{ TotalWeight: sdk.NewInt(70), Records: []types.DistrRecord{ // 30 is abstained diff --git a/x/streamer/types/expected_keepers.go b/x/streamer/types/expected_keepers.go index 39ac4c604..cd40ea8e0 100644 --- a/x/streamer/types/expected_keepers.go +++ b/x/streamer/types/expected_keepers.go @@ -16,6 +16,7 @@ import ( type BankKeeper interface { GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins + SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error } // EpochKeeper defines the expected interface needed to retrieve epoch info. @@ -33,11 +34,10 @@ type AccountKeeper interface { type IncentivesKeeper interface { CreateGauge(ctx sdk.Context, isPerpetual bool, owner sdk.AccAddress, coins sdk.Coins, distrTo lockuptypes.QueryCondition, startTime time.Time, numEpochsPaidOver uint64) (uint64, error) CreateRollappGauge(ctx sdk.Context, rollappId string) (uint64, error) - GetLockableDurations(ctx sdk.Context) []time.Duration - GetGaugeByID(ctx sdk.Context, gaugeID uint64) (*incentivestypes.Gauge, error) - AddToGaugeRewardsByID(ctx sdk.Context, owner sdk.AccAddress, coins sdk.Coins, gaugeID uint64) error + Distribute(ctx sdk.Context, gauges []incentivestypes.Gauge, cache incentivestypes.DenomLocksCache, epochEnd bool) (sdk.Coins, error) + GetDistributeToBaseLocks(ctx sdk.Context, gauge incentivestypes.Gauge, cache incentivestypes.DenomLocksCache) []lockuptypes.PeriodLock } type SponsorshipKeeper interface { diff --git a/x/streamer/types/stream.go b/x/streamer/types/stream.go index 072f32e0a..ed9b868dc 100644 --- a/x/streamer/types/stream.go +++ b/x/streamer/types/stream.go @@ -8,7 +8,7 @@ import ( ) // NewStream creates a new stream struct given the required stream parameters. -func NewStream(id uint64, distrTo *DistrInfo, coins sdk.Coins, startTime time.Time, epochIdentifier string, numEpochsPaidOver uint64, sponsored bool) Stream { +func NewStream(id uint64, distrTo DistrInfo, coins sdk.Coins, startTime time.Time, epochIdentifier string, numEpochsPaidOver uint64, sponsored bool) Stream { return Stream{ Id: id, DistributeTo: distrTo, @@ -40,3 +40,11 @@ func (stream Stream) IsActiveStream(curTime time.Time) bool { func (stream Stream) IsFinishedStream(curTime time.Time) bool { return !stream.IsUpcomingStream(curTime) && !stream.IsActiveStream(curTime) } + +func (stream *Stream) AddDistributedCoins(coins sdk.Coins) { + stream.DistributedCoins = stream.DistributedCoins.Add(coins...) +} + +func (stream Stream) Key() uint64 { + return stream.Id +} diff --git a/x/streamer/types/stream.pb.go b/x/streamer/types/stream.pb.go index 167248617..a9ca26634 100644 --- a/x/streamer/types/stream.pb.go +++ b/x/streamer/types/stream.pb.go @@ -37,7 +37,7 @@ type Stream struct { // id is the unique ID of a Stream Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` // distribute_to is the distr_info. - DistributeTo *DistrInfo `protobuf:"bytes,2,opt,name=distribute_to,json=distributeTo,proto3" json:"distribute_to,omitempty"` + DistributeTo DistrInfo `protobuf:"bytes,2,opt,name=distribute_to,json=distributeTo,proto3" json:"distribute_to"` // coins is the total amount of coins that have been in the stream // Can distribute multiple coin denoms Coins github_com_cosmos_cosmos_sdk_types.Coins `protobuf:"bytes,3,rep,name=coins,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.Coins" json:"coins"` @@ -100,11 +100,11 @@ func (m *Stream) GetId() uint64 { return 0 } -func (m *Stream) GetDistributeTo() *DistrInfo { +func (m *Stream) GetDistributeTo() DistrInfo { if m != nil { return m.DistributeTo } - return nil + return DistrInfo{} } func (m *Stream) GetCoins() github_com_cosmos_cosmos_sdk_types.Coins { @@ -172,41 +172,41 @@ func init() { } var fileDescriptor_19586ad841c00cd9 = []byte{ - // 533 bytes of a gzipped FileDescriptorProto + // 535 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x53, 0xcb, 0x6e, 0x13, 0x31, - 0x14, 0xcd, 0xa4, 0x4d, 0x68, 0x9c, 0x16, 0x11, 0x2b, 0x42, 0x43, 0x44, 0x67, 0x42, 0xd8, 0x8c, - 0x10, 0xd8, 0x7d, 0x88, 0x0d, 0xcb, 0x00, 0x8b, 0xae, 0x82, 0x86, 0x4a, 0x20, 0x36, 0x23, 0x4f, + 0x14, 0xcd, 0xa4, 0x49, 0x68, 0x9c, 0x16, 0x11, 0x2b, 0x42, 0x43, 0x44, 0x67, 0x42, 0xd8, 0x8c, + 0x10, 0xd8, 0x7d, 0x88, 0x0d, 0xcb, 0x00, 0x8b, 0xae, 0x8a, 0x86, 0x22, 0x10, 0x9b, 0xd1, 0x4c, 0xec, 0xa4, 0x16, 0x99, 0xf1, 0xc8, 0xf6, 0x44, 0x09, 0x5f, 0xd1, 0xef, 0xe0, 0x4b, 0xba, 0xec, - 0x12, 0x09, 0x29, 0x45, 0xc9, 0x1f, 0xf4, 0x0b, 0xd0, 0xd8, 0x79, 0x09, 0x51, 0x75, 0xd3, 0x55, - 0xec, 0x7b, 0xcf, 0x39, 0xf7, 0x9e, 0x93, 0x31, 0x78, 0x4d, 0xa7, 0x09, 0x4b, 0x15, 0x17, 0xe9, - 0x64, 0xfa, 0x03, 0xaf, 0x2f, 0x58, 0x69, 0xc9, 0x48, 0xc2, 0xe4, 0xf2, 0x80, 0x32, 0x29, 0xb4, - 0x80, 0xfe, 0x36, 0x1a, 0xad, 0x2f, 0x68, 0x85, 0x6e, 0x35, 0x87, 0x62, 0x28, 0x0c, 0x16, 0x17, - 0x27, 0x4b, 0x6b, 0x79, 0x43, 0x21, 0x86, 0x23, 0x86, 0xcd, 0x2d, 0xce, 0x07, 0x98, 0xe6, 0x92, - 0xe8, 0x82, 0x68, 0xfb, 0xfe, 0xbf, 0x7d, 0xcd, 0x13, 0xa6, 0x34, 0x49, 0xb2, 0x95, 0x40, 0x5f, - 0xa8, 0x44, 0x28, 0x1c, 0x13, 0xc5, 0xf0, 0xf8, 0x38, 0x66, 0x9a, 0x1c, 0xe3, 0xbe, 0xe0, 0x2b, - 0x81, 0xa3, 0xfb, 0x5c, 0x50, 0xae, 0xb4, 0x8c, 0x78, 0x3a, 0x58, 0xae, 0xd4, 0xf9, 0x5d, 0x01, - 0xd5, 0xcf, 0xa6, 0x0b, 0x1f, 0x83, 0x32, 0xa7, 0xae, 0xd3, 0x76, 0x82, 0xdd, 0xb0, 0xcc, 0x29, - 0xec, 0x81, 0x03, 0x03, 0xe7, 0x71, 0xae, 0x59, 0xa4, 0x85, 0x5b, 0x6e, 0x3b, 0x41, 0xfd, 0xe4, - 0x15, 0xba, 0xc7, 0x3c, 0xfa, 0x50, 0xb0, 0xce, 0xd2, 0x81, 0x08, 0xf7, 0x37, 0x02, 0xe7, 0x02, - 0x12, 0x50, 0x29, 0x76, 0x55, 0xee, 0x4e, 0x7b, 0x27, 0xa8, 0x9f, 0x3c, 0x43, 0xd6, 0x0d, 0x2a, - 0xdc, 0xa0, 0xa5, 0x1b, 0xf4, 0x5e, 0xf0, 0xb4, 0x7b, 0x74, 0x35, 0xf3, 0x4b, 0x3f, 0x6f, 0xfc, - 0x60, 0xc8, 0xf5, 0x45, 0x1e, 0xa3, 0xbe, 0x48, 0xf0, 0xd2, 0xba, 0xfd, 0x79, 0xa3, 0xe8, 0x77, - 0xac, 0xa7, 0x19, 0x53, 0x86, 0xa0, 0x42, 0xab, 0x0c, 0xbf, 0x02, 0xa0, 0x34, 0x91, 0x3a, 0x2a, - 0x92, 0x73, 0x77, 0xcd, 0xc2, 0x2d, 0x64, 0x63, 0x45, 0xab, 0x58, 0xd1, 0xf9, 0x2a, 0xd6, 0xee, - 0x61, 0x31, 0xe8, 0x76, 0xe6, 0x37, 0xa6, 0x24, 0x19, 0xbd, 0xeb, 0x6c, 0xb8, 0x9d, 0xcb, 0x1b, - 0xdf, 0x09, 0x6b, 0xa6, 0x50, 0xc0, 0xe1, 0x17, 0xf0, 0xd4, 0x86, 0xc7, 0x32, 0xd1, 0xbf, 0x88, - 0x38, 0x65, 0xa9, 0xe6, 0x03, 0xce, 0xa4, 0x5b, 0x69, 0x3b, 0x41, 0xad, 0xfb, 0xe2, 0x76, 0xe6, - 0x1f, 0x5a, 0x95, 0xff, 0xe3, 0x3a, 0x61, 0xd3, 0x34, 0x3e, 0x16, 0xf5, 0xb3, 0x75, 0x19, 0x62, - 0xd0, 0x4c, 0xf3, 0xc4, 0xc2, 0x55, 0x94, 0x11, 0x4e, 0x23, 0x31, 0x66, 0xd2, 0xad, 0x9a, 0x3f, - 0xa2, 0x91, 0xe6, 0x89, 0x61, 0xa8, 0x4f, 0x84, 0xd3, 0xde, 0x98, 0x49, 0xf8, 0x12, 0x1c, 0x0c, - 0xf8, 0x68, 0xc4, 0xe8, 0x92, 0xe3, 0x3e, 0x32, 0xc8, 0x7d, 0x5b, 0xb4, 0x60, 0x38, 0x01, 0x8d, - 0x4d, 0xf6, 0x34, 0xb2, 0xb9, 0xef, 0x3d, 0x7c, 0xee, 0x4f, 0xb6, 0xa6, 0x98, 0x0a, 0x7c, 0x0e, - 0x6a, 0x2a, 0x13, 0xa9, 0x12, 0x92, 0x51, 0xb7, 0xd6, 0x76, 0x82, 0xbd, 0x70, 0x53, 0x80, 0x23, - 0x50, 0xb7, 0xc1, 0xd8, 0x8d, 0xc0, 0xc3, 0x6f, 0x04, 0x8c, 0xbe, 0x39, 0x77, 0x7b, 0x57, 0x73, - 0xcf, 0xb9, 0x9e, 0x7b, 0xce, 0x9f, 0xb9, 0xe7, 0x5c, 0x2e, 0xbc, 0xd2, 0xf5, 0xc2, 0x2b, 0xfd, - 0x5a, 0x78, 0xa5, 0x6f, 0x6f, 0xb7, 0xf4, 0xee, 0x78, 0x34, 0xe3, 0x53, 0x3c, 0xd9, 0xbc, 0x1c, - 0x33, 0x22, 0xae, 0x9a, 0x6f, 0xe8, 0xf4, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x95, 0x10, 0x61, - 0xc8, 0x2f, 0x04, 0x00, 0x00, + 0x92, 0x55, 0x0a, 0xc9, 0x1f, 0xf4, 0x0b, 0x90, 0xed, 0xbc, 0x84, 0x40, 0xdd, 0x74, 0x15, 0xfb, + 0xdc, 0x73, 0xce, 0xbd, 0xf7, 0x64, 0x0c, 0x5e, 0x92, 0x69, 0x4a, 0x33, 0xc9, 0x78, 0x36, 0x99, + 0x7e, 0xc7, 0xeb, 0x0b, 0x96, 0x4a, 0xd0, 0x38, 0xa5, 0x62, 0x79, 0x40, 0xb9, 0xe0, 0x8a, 0x43, + 0x7f, 0x9b, 0x8d, 0xd6, 0x17, 0xb4, 0x62, 0xb7, 0x5b, 0x43, 0x3e, 0xe4, 0x86, 0x8b, 0xf5, 0xc9, + 0xca, 0xda, 0xde, 0x90, 0xf3, 0xe1, 0x88, 0x62, 0x73, 0x4b, 0x8a, 0x01, 0x26, 0x85, 0x88, 0x95, + 0x16, 0xda, 0xba, 0xff, 0x77, 0x5d, 0xb1, 0x94, 0x4a, 0x15, 0xa7, 0xf9, 0xca, 0xa0, 0xcf, 0x65, + 0xca, 0x25, 0x4e, 0x62, 0x49, 0xf1, 0xf8, 0x28, 0xa1, 0x2a, 0x3e, 0xc2, 0x7d, 0xce, 0x56, 0x06, + 0x87, 0x77, 0x6d, 0x41, 0x98, 0x54, 0x22, 0x62, 0xd9, 0x60, 0x39, 0x52, 0xf7, 0x77, 0x15, 0xd4, + 0x3e, 0x9a, 0x2a, 0x7c, 0x08, 0xca, 0x8c, 0xb8, 0x4e, 0xc7, 0x09, 0x2a, 0x61, 0x99, 0x11, 0xf8, + 0x09, 0xec, 0x1b, 0x3a, 0x4b, 0x0a, 0x45, 0x23, 0xc5, 0xdd, 0x72, 0xc7, 0x09, 0x1a, 0xc7, 0x2f, + 0xd0, 0x1d, 0xcb, 0xa3, 0x77, 0x5a, 0x75, 0x9a, 0x0d, 0x78, 0xaf, 0x72, 0x35, 0xf3, 0x4b, 0xe1, + 0xde, 0xc6, 0xe6, 0x9c, 0xc3, 0x18, 0x54, 0xf5, 0xc4, 0xd2, 0xdd, 0xe9, 0xec, 0x04, 0x8d, 0xe3, + 0x27, 0xc8, 0xee, 0x84, 0xf4, 0x4e, 0x68, 0xb9, 0x13, 0x7a, 0xcb, 0x59, 0xd6, 0x3b, 0xd4, 0xea, + 0x1f, 0x37, 0x7e, 0x30, 0x64, 0xea, 0xa2, 0x48, 0x50, 0x9f, 0xa7, 0x78, 0x19, 0x80, 0xfd, 0x79, + 0x25, 0xc9, 0x37, 0xac, 0xa6, 0x39, 0x95, 0x46, 0x20, 0x43, 0xeb, 0x0c, 0xbf, 0x00, 0x20, 0x55, + 0x2c, 0x54, 0xa4, 0xf3, 0x73, 0x2b, 0x66, 0xec, 0x36, 0xb2, 0xe1, 0xa2, 0x55, 0xb8, 0xe8, 0x7c, + 0x15, 0x6e, 0xef, 0x40, 0x37, 0xba, 0x9d, 0xf9, 0xcd, 0x69, 0x9c, 0x8e, 0xde, 0x74, 0x37, 0xda, + 0xee, 0xe5, 0x8d, 0xef, 0x84, 0x75, 0x03, 0x68, 0x3a, 0xfc, 0x0c, 0x1e, 0xdb, 0x08, 0x69, 0xce, + 0xfb, 0x17, 0x11, 0x23, 0x34, 0x53, 0x6c, 0xc0, 0xa8, 0x70, 0xab, 0x1d, 0x27, 0xa8, 0xf7, 0x9e, + 0xdd, 0xce, 0xfc, 0x03, 0xeb, 0xf2, 0x6f, 0x5e, 0x37, 0x6c, 0x99, 0xc2, 0x7b, 0x8d, 0x9f, 0xae, + 0x61, 0x88, 0x41, 0x2b, 0x2b, 0x52, 0x4b, 0x97, 0x51, 0x1e, 0x33, 0x12, 0xf1, 0x31, 0x15, 0x6e, + 0xcd, 0xfc, 0x1d, 0xcd, 0xac, 0x48, 0x8d, 0x42, 0x7e, 0x88, 0x19, 0x39, 0x1b, 0x53, 0x01, 0x9f, + 0x83, 0xfd, 0x01, 0x1b, 0x8d, 0x28, 0x59, 0x6a, 0xdc, 0x07, 0x86, 0xb9, 0x67, 0x41, 0x4b, 0x86, + 0x13, 0xd0, 0xdc, 0x64, 0x4f, 0x22, 0x9b, 0xfb, 0xee, 0xfd, 0xe7, 0xfe, 0x68, 0xab, 0x8b, 0x41, + 0xe0, 0x53, 0x50, 0x97, 0x39, 0xcf, 0x24, 0x17, 0x94, 0xb8, 0xf5, 0x8e, 0x13, 0xec, 0x86, 0x1b, + 0x00, 0x8e, 0x40, 0xc3, 0x06, 0x63, 0x27, 0x02, 0xf7, 0x3f, 0x11, 0x30, 0xfe, 0xe6, 0xdc, 0x3b, + 0xbb, 0x9a, 0x7b, 0xce, 0xf5, 0xdc, 0x73, 0x7e, 0xcd, 0x3d, 0xe7, 0x72, 0xe1, 0x95, 0xae, 0x17, + 0x5e, 0xe9, 0xe7, 0xc2, 0x2b, 0x7d, 0x7d, 0xbd, 0xe5, 0xf7, 0x9f, 0xa7, 0x33, 0x3e, 0xc1, 0x93, + 0xcd, 0xfb, 0x31, 0x2d, 0x92, 0x9a, 0xf9, 0x86, 0x4e, 0xfe, 0x04, 0x00, 0x00, 0xff, 0xff, 0xbc, + 0x5a, 0x10, 0x3d, 0x35, 0x04, 0x00, 0x00, } func (m *Stream) Marshal() (dAtA []byte, err error) { @@ -306,18 +306,16 @@ func (m *Stream) MarshalToSizedBuffer(dAtA []byte) (int, error) { dAtA[i] = 0x1a } } - if m.DistributeTo != nil { - { - size, err := m.DistributeTo.MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintStream(dAtA, i, uint64(size)) + { + size, err := m.DistributeTo.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err } - i-- - dAtA[i] = 0x12 + i -= size + i = encodeVarintStream(dAtA, i, uint64(size)) } + i-- + dAtA[i] = 0x12 if m.Id != 0 { i = encodeVarintStream(dAtA, i, uint64(m.Id)) i-- @@ -346,10 +344,8 @@ func (m *Stream) Size() (n int) { if m.Id != 0 { n += 1 + sovStream(uint64(m.Id)) } - if m.DistributeTo != nil { - l = m.DistributeTo.Size() - n += 1 + l + sovStream(uint64(l)) - } + l = m.DistributeTo.Size() + n += 1 + l + sovStream(uint64(l)) if len(m.Coins) > 0 { for _, e := range m.Coins { l = e.Size() @@ -469,9 +465,6 @@ func (m *Stream) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - if m.DistributeTo == nil { - m.DistributeTo = &DistrInfo{} - } if err := m.DistributeTo.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err }