Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: CNS-daily restaking credit #1794

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions proto/lavanet/lava/dualstaking/delegate.proto
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ message Delegation {
string delegator = 3; // delegator that owns the delegated funds
cosmos.base.v1beta1.Coin amount = 4 [(gogoproto.nullable) = false];
int64 timestamp = 5; // Unix timestamp of the delegation (+ month)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix comment, current time, not +month

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the last change timestamp

cosmos.base.v1beta1.Coin credit = 6 [(gogoproto.nullable) = false]; // amount of credit earned by the delegation over the period
Yaroms marked this conversation as resolved.
Show resolved Hide resolved
int64 credit_timestamp = 7; // Unix timestamp of the delegation credit start
}

message Delegator {
Expand Down
8 changes: 8 additions & 0 deletions testutil/common/tester.go
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,14 @@ func (ts *Tester) AdvanceMonthsFrom(from time.Time, months int) *Tester {
return ts
}

func (ts *Tester) AdvanceTimeHours(timeDelta time.Duration) *Tester {
endTime := ts.BlockTime().Add(timeDelta)
for ts.BlockTime().Before(endTime) {
ts.AdvanceBlock(time.Hour)
}
return ts
}

func (ts *Tester) BondDenom() string {
return ts.Keepers.StakingKeeper.BondDenom(sdk.UnwrapSDKContext(ts.Ctx))
}
Expand Down
5 changes: 3 additions & 2 deletions testutil/keeper/dualstaking.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keeper

import (
"testing"
"time"

tmdb "github.com/cometbft/cometbft-db"
"github.com/cometbft/cometbft/libs/log"
Expand Down Expand Up @@ -64,15 +65,15 @@ func DualstakingKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) {
memStoreKey,
paramsSubspace,
&mockBankKeeper{},
nil,
&mockStakingKeeperEmpty{},
&mockAccountKeeper{},
epochstorageKeeper,
speckeeper.NewKeeper(cdc, nil, nil, paramsSubspaceSpec, nil),
fixationkeeper.NewKeeper(cdc, tsKeeper, epochstorageKeeper.BlocksToSaveRaw),
)

ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger())

ctx = ctx.WithBlockTime(time.Now().UTC())
// Initialize params
k.SetParams(ctx, types.DefaultParams())

Expand Down
56 changes: 56 additions & 0 deletions testutil/keeper/mock_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"fmt"
"time"

"cosmossdk.io/math"
tenderminttypes "github.com/cometbft/cometbft/types"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)

// account keeper mock
Expand Down Expand Up @@ -36,6 +38,60 @@ func (k mockAccountKeeper) SetModuleAccount(sdk.Context, authtypes.ModuleAccount
// mock bank keeper
var balance map[string]sdk.Coins = make(map[string]sdk.Coins)

type mockStakingKeeperEmpty struct{}

func (k mockStakingKeeperEmpty) ValidatorByConsAddr(sdk.Context, sdk.ConsAddress) stakingtypes.ValidatorI {
return nil
}

func (k mockStakingKeeperEmpty) UnbondingTime(ctx sdk.Context) time.Duration {
return time.Duration(0)
}

func (k mockStakingKeeperEmpty) GetAllDelegatorDelegations(ctx sdk.Context, delegator sdk.AccAddress) []stakingtypes.Delegation {
return nil
}

func (k mockStakingKeeperEmpty) GetDelegatorValidator(ctx sdk.Context, delegatorAddr sdk.AccAddress, validatorAddr sdk.ValAddress) (validator stakingtypes.Validator, err error) {
return stakingtypes.Validator{}, nil
}

func (k mockStakingKeeperEmpty) GetDelegation(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) (delegation stakingtypes.Delegation, found bool) {
return stakingtypes.Delegation{}, false
}

func (k mockStakingKeeperEmpty) GetValidator(ctx sdk.Context, addr sdk.ValAddress) (validator stakingtypes.Validator, found bool) {
return stakingtypes.Validator{}, false
}

func (k mockStakingKeeperEmpty) GetValidatorDelegations(ctx sdk.Context, valAddr sdk.ValAddress) (delegations []stakingtypes.Delegation) {
return []stakingtypes.Delegation{}
}

func (k mockStakingKeeperEmpty) BondDenom(ctx sdk.Context) string {
return "ulava"
}

func (k mockStakingKeeperEmpty) ValidateUnbondAmount(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, amt math.Int) (shares sdk.Dec, err error) {
return sdk.Dec{}, nil
}

func (k mockStakingKeeperEmpty) Undelegate(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, sharesAmount sdk.Dec) (time.Time, error) {
return time.Time{}, nil
}

func (k mockStakingKeeperEmpty) Delegate(ctx sdk.Context, delAddr sdk.AccAddress, bondAmt math.Int, tokenSrc stakingtypes.BondStatus, validator stakingtypes.Validator, subtractAccount bool) (newShares sdk.Dec, err error) {
return sdk.Dec{}, nil
}

func (k mockStakingKeeperEmpty) GetBondedValidatorsByPower(ctx sdk.Context) []stakingtypes.Validator {
return []stakingtypes.Validator{}
}

func (k mockStakingKeeperEmpty) GetAllValidators(ctx sdk.Context) (validators []stakingtypes.Validator) {
return []stakingtypes.Validator{}
}

type mockBankKeeper struct{}

func init_balance() {
Expand Down
17 changes: 13 additions & 4 deletions x/dualstaking/keeper/delegate.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,14 @@ import (
// and updates the (epochstorage) stake-entry.
func (k Keeper) increaseDelegation(ctx sdk.Context, delegator, provider string, amount sdk.Coin, stake bool) error {
// get, update the delegation entry
delegation, err := k.delegations.Get(ctx, types.DelegationKey(provider, delegator))
if err != nil {
delegation, found := k.GetDelegation(ctx, provider, delegator)
if !found {
// new delegation (i.e. not increase of existing one)
delegation = types.NewDelegation(delegator, provider, ctx.BlockTime(), k.stakingKeeper.BondDenom(ctx))
}

delegation.AddAmount(amount)

err = k.delegations.Set(ctx, types.DelegationKey(provider, delegator), delegation)
err := k.SetDelegation(ctx, delegation)
if err != nil {
return err
}
Expand Down Expand Up @@ -364,7 +363,17 @@ func (k Keeper) GetAllDelegations(ctx sdk.Context) ([]types.Delegation, error) {
return iter.Values()
}

// this function overwrites the time tag with the ctx time upon writing the delegation
func (k Keeper) SetDelegation(ctx sdk.Context, delegation types.Delegation) error {
delegation.Timestamp = ctx.BlockTime().UTC().Unix()
existingDelegation, found := k.GetDelegation(ctx, delegation.Provider, delegation.Delegator)
if !found {
return k.delegations.Set(ctx, types.DelegationKey(delegation.Provider, delegation.Delegator), delegation)
}
// calculate credit based on the existing delegation before changes
credit, creditTimestamp := k.CalculateCredit(ctx, existingDelegation)
delegation.Credit = credit
delegation.CreditTimestamp = creditTimestamp
return k.delegations.Set(ctx, types.DelegationKey(delegation.Provider, delegation.Delegator), delegation)
}

Expand Down
107 changes: 107 additions & 0 deletions x/dualstaking/keeper/delegate_credit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package keeper

// Delegation allows securing funds for a specific provider to effectively increase
// its stake so it will be paired with consumers more often. The delegators do not
// transfer the funds to the provider but only bestow the funds with it. In return
// to locking the funds there, delegators get some of the provider’s profit (after
// commission deduction).
//
// The delegated funds are stored in the module's BondedPoolName account. On request
// to terminate the delegation, they are then moved to the modules NotBondedPoolName
// account, and remain locked there for staking.UnbondingTime() witholding period
// before finally released back to the delegator. The timers for bonded funds are
// tracked are indexed by the delegator, provider, and chainID.
//
// The delegation state is stores with fixation using two maps: one for delegations
// indexed by the combination <provider,chainD,delegator>, used to track delegations
// and find/access delegations by provider (and chainID); and another for delegators
// tracking the list of providers for a delegator, indexed by the delegator.

import (
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/lavanet/lava/v4/x/dualstaking/types"
)

const (
monthHours = 720 // 30 days * 24 hours
hourSeconds = 3600
)

// calculate the delegation credit based on the timestamps, and the amounts of delegations
// amounts and credits represent daily value, rounded down
// can be used to calculate the credit for distribution or update the credit fields in the delegation
func (k Keeper) CalculateCredit(ctx sdk.Context, delegation types.Delegation) (credit sdk.Coin, creditTimestampRet int64) {
// Calculate the credit for the delegation
currentAmount := delegation.Amount
creditAmount := delegation.Credit
// handle uninitialized amounts
if creditAmount.IsNil() {
creditAmount = sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), sdk.ZeroInt())
}
if currentAmount.IsNil() {
// this should never happen, but we handle it just in case
currentAmount = sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), sdk.ZeroInt())
}
currentTimestamp := ctx.BlockTime().UTC()
delegationTimestamp := time.Unix(delegation.Timestamp, 0)
creditTimestamp := time.Unix(delegation.CreditTimestamp, 0)
// we normalize dates before we start the calculation
// maximum scope is 30 days, we start with the delegation truncation then the credit
monthAgo := currentTimestamp.AddDate(0, 0, -30) // we are doing 30 days not a month a month can be a different amount of days
if monthAgo.After(delegationTimestamp) {
// in the case the delegation wasn't changed for 30 days or more we truncate the timestamp to 30 days ago
// and disable the credit for older dates since they are irrelevant
delegationTimestamp = monthAgo
creditTimestamp = delegationTimestamp
creditAmount = sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), sdk.ZeroInt())
} else if monthAgo.After(creditTimestamp) {
// delegation is less than 30 days, but credit might be older, so truncate it to 30 days
creditTimestamp = monthAgo
Yaroms marked this conversation as resolved.
Show resolved Hide resolved
}

creditDelta := int64(0) // hours
if delegation.CreditTimestamp == 0 || creditAmount.IsZero() {
Yaroms marked this conversation as resolved.
Show resolved Hide resolved
// in case credit was never set, we set it to the delegation timestamp
creditTimestamp = delegationTimestamp
} else if creditTimestamp.Before(delegationTimestamp) {
// calculate the credit delta in hours
creditDelta = (delegationTimestamp.Unix() - creditTimestamp.Unix()) / hourSeconds
}

amountDelta := int64(0) // hours
if !currentAmount.IsZero() && delegationTimestamp.Before(currentTimestamp) {
amountDelta = (currentTimestamp.Unix() - delegationTimestamp.Unix()) / hourSeconds
}

// creditDelta is the weight of the history and amountDelta is the weight of the current amount
// we need to average them and store it in the credit
totalDelta := creditDelta + amountDelta
if totalDelta == 0 {
return sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), sdk.ZeroInt()), currentTimestamp.Unix()
}
credit = sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), currentAmount.Amount.MulRaw(amountDelta).Add(creditAmount.Amount.MulRaw(creditDelta)).QuoRaw(totalDelta))
return credit, creditTimestamp.Unix()
}

// this function takes the delegation and returns it's credit within the last 30 days
func (k Keeper) CalculateMonthlyCredit(ctx sdk.Context, delegation types.Delegation) (credit sdk.Coin) {
credit, creditTimeEpoch := k.CalculateCredit(ctx, delegation)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is it called "creditTimeEpoch"?

if credit.IsNil() || credit.IsZero() || creditTimeEpoch <= 0 {
return sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), sdk.ZeroInt())
}
creditTimestamp := time.Unix(creditTimeEpoch, 0)
timeStampDiff := (ctx.BlockTime().UTC().Unix() - creditTimestamp.Unix()) / hourSeconds
if timeStampDiff <= 0 {
// no positive credit
return sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), sdk.ZeroInt())
}
// make sure we never increase the credit
if timeStampDiff > monthHours {
timeStampDiff = monthHours
}
// normalize credit to 30 days
credit.Amount = credit.Amount.MulRaw(timeStampDiff).QuoRaw(monthHours)
return credit
}
Loading
Loading