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

[CORE-824] x/ratelimit: Implement UpdateCapacityEndBlocker #941

Merged
merged 6 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
23 changes: 12 additions & 11 deletions protocol/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -588,10 +588,22 @@ func New(
transferModule := transfer.NewAppModule(app.TransferKeeper)
transferIBCModule := transfer.NewIBCModule(app.TransferKeeper)

app.BlockTimeKeeper = *blocktimemodulekeeper.NewKeeper(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This code block is moved from below to before the app.RateLimitKeeper initialization.

appCodec,
keys[blocktimemoduletypes.StoreKey],
// set the governance and delaymsg module accounts as the authority for conducting upgrades
[]string{
lib.GovModuleAddress.String(),
delaymsgmoduletypes.ModuleAddress.String(),
},
)
blockTimeModule := blocktimemodule.NewAppModule(appCodec, app.BlockTimeKeeper)

app.RatelimitKeeper = *ratelimitmodulekeeper.NewKeeper(
appCodec,
keys[ratelimitmoduletypes.StoreKey],
app.BankKeeper,
app.BlockTimeKeeper,
// set the governance and delaymsg module accounts as the authority for conducting upgrades
[]string{
lib.GovModuleAddress.String(),
Expand Down Expand Up @@ -792,17 +804,6 @@ func New(
)
assetsModule := assetsmodule.NewAppModule(appCodec, app.AssetsKeeper)

app.BlockTimeKeeper = *blocktimemodulekeeper.NewKeeper(
appCodec,
keys[blocktimemoduletypes.StoreKey],
// set the governance and delaymsg module accounts as the authority for conducting upgrades
[]string{
lib.GovModuleAddress.String(),
delaymsgmoduletypes.ModuleAddress.String(),
},
)
blockTimeModule := blocktimemodule.NewAppModule(appCodec, app.BlockTimeKeeper)

app.DelayMsgKeeper = *delaymsgmodulekeeper.NewKeeper(
appCodec,
keys[delaymsgmoduletypes.StoreKey],
Expand Down
5 changes: 5 additions & 0 deletions protocol/lib/metrics/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,11 @@ const (
ValidatorNumFills = "validator_num_fills"
ValidatorNumMatchedTakerOrders = "validator_num_matched_taker_orders"
ValidatorVolumeQuoteQuantums = "validator_volume_quote_quantums"

// x/ratelimit
Capacity = "capacity"
RateLimitDenom = "rate_limit_denom"
LimiterIndex = "limiter_index"
)

const (
Expand Down
6 changes: 6 additions & 0 deletions protocol/x/blocktime/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ func (k Keeper) GetPreviousBlockInfo(ctx sdk.Context) types.BlockInfo {
return info
}

// GetTimeSinceLastBlock returns the time delta between the current block time and the last block time.
func (k Keeper) GetTimeSinceLastBlock(ctx sdk.Context) time.Duration {
prevBlockInfo := k.GetPreviousBlockInfo(ctx)
return ctx.BlockTime().Sub(prevBlockInfo.Timestamp)
}

func (k Keeper) SetPreviousBlockInfo(ctx sdk.Context, info *types.BlockInfo) {
store := ctx.KVStore(k.storeKey)
b := k.cdc.MustMarshal(info)
Expand Down
43 changes: 43 additions & 0 deletions protocol/x/blocktime/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,46 @@ func TestGetDowntimeInfoFor(t *testing.T) {
})
}
}

func TestGetTimeSinceLastBlock(t *testing.T) {
testPrevBlockHeight := uint32(5)
tests := map[string]struct {
prevBlockTime time.Time
currBlockTime time.Time
expectedTimeSinceLastBlock time.Duration
}{
"2 sec": {
prevBlockTime: time.Unix(100, 0).UTC(),
currBlockTime: time.Unix(102, 0).UTC(),
expectedTimeSinceLastBlock: time.Second * 2,
},
"Realistic values": {
prevBlockTime: time.Unix(1_704_827_023, 123_000_000).UTC(),
currBlockTime: time.Unix(1_704_827_024, 518_000_000).UTC(),
expectedTimeSinceLastBlock: time.Second*1 + time.Nanosecond*395_000_000,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
tApp := testapp.NewTestAppBuilder(t).Build()
tApp.InitChain()

ctx := tApp.AdvanceToBlock(
testPrevBlockHeight,
testapp.AdvanceToBlockOptions{
BlockTime: tc.prevBlockTime,
},
)

k := tApp.App.BlockTimeKeeper

actual := k.GetTimeSinceLastBlock(ctx.WithBlockTime(tc.currBlockTime))
require.Equal(
t,
tc.expectedTimeSinceLastBlock,
actual,
)
})
}
}
3 changes: 2 additions & 1 deletion protocol/x/blocktime/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package blocktime

import (
"context"
"cosmossdk.io/core/appmodule"
"encoding/json"
"fmt"

"cosmossdk.io/core/appmodule"

"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/spf13/cobra"

Expand Down
125 changes: 95 additions & 30 deletions protocol/x/ratelimit/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,22 @@ import (
"cosmossdk.io/store/prefix"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/telemetry"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/dydxprotocol/v4-chain/protocol/dtypes"
"github.com/dydxprotocol/v4-chain/protocol/lib"
"github.com/dydxprotocol/v4-chain/protocol/lib/metrics"
"github.com/dydxprotocol/v4-chain/protocol/x/ratelimit/types"
ratelimitutil "github.com/dydxprotocol/v4-chain/protocol/x/ratelimit/util"
gometrics "github.com/hashicorp/go-metrics"
)

type (
Keeper struct {
cdc codec.BinaryCodec
storeKey storetypes.StoreKey
bankKeeper types.BankKeeper
cdc codec.BinaryCodec
storeKey storetypes.StoreKey
bankKeeper types.BankKeeper
blockTimeKeeper types.BlockTimeKeeper

// TODO(CORE-824): Implement `x/ratelimit` keeper

Expand All @@ -32,13 +37,15 @@ func NewKeeper(
cdc codec.BinaryCodec,
storeKey storetypes.StoreKey,
bankKeeper types.BankKeeper,
blockTimeKeeper types.BlockTimeKeeper,
authorities []string,
) *Keeper {
return &Keeper{
cdc: cdc,
storeKey: storeKey,
bankKeeper: bankKeeper,
authorities: lib.UniqueSliceToSet(authorities),
cdc: cdc,
storeKey: storeKey,
bankKeeper: bankKeeper,
blockTimeKeeper: blockTimeKeeper,
authorities: lib.UniqueSliceToSet(authorities),
}
}

Expand Down Expand Up @@ -116,28 +123,6 @@ func (k Keeper) ProcessDeposit(
})
}

// GetBaseline returns the current capacity baseline for the given limiter.
// `baseline` formula:
//
// baseline = max(baseline_minimum, baseline_tvl_ppm * current_tvl)
func (k Keeper) GetBaseline(
ctx sdk.Context,
denom string,
limiter types.Limiter,
) *big.Int {
// Get the current TVL.
supply := k.bankKeeper.GetSupply(ctx, denom)
currentTVL := supply.Amount.BigInt()

return lib.BigMax(
limiter.BaselineMinimum.BigInt(),
lib.BigIntMulPpm(
currentTVL,
limiter.BaselineTvlPpm,
),
)
}

// SetLimitParams sets `LimitParams` for the given denom.
// Also overwrites the existing `DenomCapacity` object for the denom with a default `capacity_list` of the
// same length as the `limiters` list. Each `capacity` is initialized to the current baseline.
Expand All @@ -161,11 +146,12 @@ func (k Keeper) SetLimitParams(
return
}

currentTvl := k.bankKeeper.GetSupply(ctx, limitParams.Denom)
// Initialize the capacity list with the current baseline.
newCapacityList := make([]dtypes.SerializableInt, len(limitParams.Limiters))
for i, limiter := range limitParams.Limiters {
newCapacityList[i] = dtypes.NewIntFromBigInt(
k.GetBaseline(ctx, limitParams.Denom, limiter),
ratelimitutil.GetBaseline(currentTvl.Amount.BigInt(), limiter),
)
}
// Set correspondong `DenomCapacity` in state.
Expand Down Expand Up @@ -216,6 +202,85 @@ func (k Keeper) SetDenomCapacity(
b := k.cdc.MustMarshal(&denomCapacity)
store.Set(key, b)
}

// Emit telemetry for the new capacity list.
for i, capacity := range denomCapacity.CapacityList {
telemetry.SetGaugeWithLabels(
[]string{types.ModuleName, metrics.Capacity},
metrics.GetMetricValueFromBigInt(capacity.BigInt()),
[]gometrics.Label{
metrics.GetLabelForStringValue(metrics.RateLimitDenom, denomCapacity.Denom),
metrics.GetLabelForIntValue(metrics.LimiterIndex, i),
},
)
}
}

// UpdateAllCapacitiesEndBlocker is called during the EndBlocker to update the capacity for all limit params.
func (k Keeper) UpdateAllCapacitiesEndBlocker(
ctx sdk.Context,
) {
// Iterate through all the limit params in state.
store := prefix.NewStore(ctx.KVStore(k.storeKey), []byte(types.LimitParamsKeyPrefix))
iterator := storetypes.KVStorePrefixIterator(store, []byte{})

defer iterator.Close()

for ; iterator.Valid(); iterator.Next() {
var limitParams types.LimitParams
k.cdc.MustUnmarshal(iterator.Value(), &limitParams)
k.updateCapacityForLimitParams(ctx, limitParams)
}
}

// updateCapacityForLimitParams calculates current baseline for a denom and recovers some amount of capacity
// towards baseline.
// Assumes that the `LimitParams` exist in state.
func (k Keeper) updateCapacityForLimitParams(
ctx sdk.Context,
limitParams types.LimitParams,
Copy link
Contributor

Choose a reason for hiding this comment

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

For safety, should we validate LimitParams and DenomCapacity values exist instead of assuming? Does it make sense to validate their values too?

Copy link
Contributor Author

@teddyding teddyding Jan 9, 2024

Choose a reason for hiding this comment

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

In general, I'm not a fan of passing values from function A to function B and then doing validations in function B. Instead the better pattern is to clearly document assumptions.

Similarly, LimitParams is an input to updateCapacityForLimitParams and therefore it is the responsibility of caller function to pass in correct value.

We actually do not assume DenomCapacity to exist in this function. If it does not, the GetDenomCapacity will return an empty list, causing the code to throw an error in if condition if len(capacityList) != len(limitParams.Limiters) . Updated comment to reflect this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Update: checking this under in CalculateNewCapacityList

) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this is a 100+ line logic method. Is there room to break this into smaller helper methods? For example:

  1. GetCapacity
  2. GetCapacityDiff
  3. CalcNewCapacity

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree this function is too large. Broke down according to @BrendanChou 's suggestion below!

tvl := k.bankKeeper.GetSupply(ctx, limitParams.Denom)

capacityList := k.GetDenomCapacity(ctx, limitParams.Denom).CapacityList
if len(capacityList) != len(limitParams.Limiters) {
// This violates an invariant. Since this is in the `EndBlocker`, we log an error instead of panicking.
k.Logger(ctx).Error(
fmt.Sprintf(
"denom (%s) capacity list length (%v) != limiters length (%v); skipping capacity update",
limitParams.Denom,
len(capacityList),
len(limitParams.Limiters),
),
)
return
}

timeSinceLastBlock := k.blockTimeKeeper.GetTimeSinceLastBlock(ctx)

if timeSinceLastBlock < 0 {
// This violates an invariant (current block time > prev block time).
// Since this is in the `EndBlocker`, we log an error instead of panicking.
k.Logger(ctx).Error(
fmt.Sprintf(
"timeSinceLastBlock (%v) <= 0; skipping capacity update",
timeSinceLastBlock,
),
)
return
}

newCapacityList := ratelimitutil.CalculateNewCapacityList(
tvl.Amount.BigInt(),
limitParams,
capacityList,
timeSinceLastBlock,
)

k.SetDenomCapacity(ctx, types.DenomCapacity{
Denom: limitParams.Denom,
CapacityList: newCapacityList,
})
}

// GetDenomCapacity returns `DenomCapacity` for the given denom.
Expand Down
Loading
Loading