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

test: fix and unit tests for query pending cctx within rate limit #2060

Merged
merged 41 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
8db24cc
initial commit of grpc pending cctx query with rate limiter
ws4charlie Apr 18, 2024
a873173
replace big.Float with sdk.Dec and update mock rate limiter flags
ws4charlie Apr 18, 2024
60cdbd0
split big loop into backwards loop and forwards loop to be more accurate
ws4charlie Apr 18, 2024
b888133
Merge branch 'develop' of https://github.com/zeta-chain/node into fea…
ws4charlie Apr 18, 2024
3f63f22
adjust zetaclient code to query pending cctx with rate limit
ws4charlie Apr 18, 2024
f0b0677
update change log and add one more rate limiter flag test
ws4charlie Apr 19, 2024
aab441e
use outboun amount for calculation
ws4charlie Apr 19, 2024
00ab5ac
some minimum code refactor
ws4charlie Apr 19, 2024
f31c988
created separate file for cctx query with rate limit
ws4charlie Apr 19, 2024
3cdf614
Merge branch 'develop' into feat-withdraw-rate-limit
ws4charlie Apr 19, 2024
6ad94af
improved a few error handlling
ws4charlie Apr 19, 2024
27e94a7
use old cctx query as fallback when rate limiter is disabled; some re…
ws4charlie Apr 19, 2024
0eb3b8a
fixed unit test compile
ws4charlie Apr 19, 2024
1568e44
Merge branch 'develop' into feat-withdraw-rate-limit
lumtis Apr 19, 2024
60e5dcc
added unit test for fallback query
ws4charlie Apr 20, 2024
101457c
added unit tests for cctx value conversion
ws4charlie Apr 20, 2024
7065d2e
added unit tests for cctx value conversion
ws4charlie Apr 20, 2024
874d5f3
add changelog entry
ws4charlie Apr 20, 2024
16955b8
added unit tests for query pending cctxs within rate limit
ws4charlie Apr 20, 2024
249bcaa
added total value in rate limiter window for monitoring purpose
ws4charlie Apr 20, 2024
dd950a8
Merge branch 'develop' into unit-test-rate-limit
lumtis Apr 22, 2024
7fe367f
Update x/crosschain/keeper/grpc_query_cctx_rate_limit.go
ws4charlie Apr 22, 2024
e5e4e28
change variable name fCoin to foreignCoin
ws4charlie Apr 22, 2024
5e28daf
Update x/fungible/keeper/foreign_coins.go
ws4charlie Apr 22, 2024
da27a84
Update x/crosschain/keeper/grpc_query_cctx_rate_limit_test.go
ws4charlie Apr 22, 2024
f9eed15
converted rate limiter query unit tests to table test
ws4charlie Apr 22, 2024
f40cf45
handle edge case when pending cctxs span wider block range than slidi…
ws4charlie Apr 24, 2024
578afcc
added zero rate check; added comment to make unit test clearer
ws4charlie Apr 24, 2024
fa24f32
added unit test and note for method GetAllForeignCoinMap
ws4charlie Apr 24, 2024
61627b1
treat Rate as average block rate; stop outbound when current rate lim…
ws4charlie Apr 25, 2024
daba269
add commented unit tests back
ws4charlie Apr 25, 2024
ec6aca8
Merge branch 'v16.0.0-rc' into unit-test-rate-limit
lumtis Apr 29, 2024
c011900
replace sdk.Dec with sdkmath.Int to represent cctx value in azeta
ws4charlie Apr 29, 2024
289ec62
test: disable header proof test in local upgrade test E2E test (#2051)
lumtis Apr 29, 2024
bd13759
test(e2e): add rate limiter admin E2E test (#2063)
lumtis Apr 29, 2024
4be83fb
removed incorrect Note
ws4charlie Apr 29, 2024
2a1433d
Merge branch 'v16.0.0-rc' into unit-test-rate-limit
ws4charlie Apr 29, 2024
23b7930
improved variable name
ws4charlie Apr 29, 2024
17b955e
Merge branch 'release/v16' into unit-test-rate-limit
lumtis Apr 30, 2024
7136f31
add E2E test for rate limiter gas and erc20
lumtis Apr 30, 2024
fd861d5
remove outdated comment
lumtis Apr 30, 2024
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
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
* [1985](https://github.com/zeta-chain/node/pull/1985) - improve fungible module coverage
* [1992](https://github.com/zeta-chain/node/pull/1992) - remove setupKeeper from crosschain module
* [2008](https://github.com/zeta-chain/node/pull/2008) - add test for connector bytecode update
* [2060](https://github.com/zeta-chain/node/pull/2060) - add unit test for rate limiter query

### Fixes

Expand Down
3 changes: 3 additions & 0 deletions docs/openapi/openapi.swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54013,6 +54013,9 @@ definitions:
total_pending:
type: string
format: uint64
value_within_window:
type: string
format: uint64
rate_limit_exceeded:
type: boolean
crosschainQueryMessagePassingProtocolFeeResponse:
Expand Down
3 changes: 2 additions & 1 deletion proto/crosschain/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,8 @@ message QueryListPendingCctxWithinRateLimitRequest {
message QueryListPendingCctxWithinRateLimitResponse {
repeated CrossChainTx cross_chain_tx = 1;
uint64 total_pending = 2;
bool rate_limit_exceeded = 3;
uint64 value_within_window = 3;
bool rate_limit_exceeded = 4;
}

message QueryLastZetaHeightRequest {}
Expand Down
6 changes: 3 additions & 3 deletions testutil/keeper/mocks/crosschain/fungible.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion typescript/crosschain/query_pb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -910,7 +910,12 @@ export declare class QueryListPendingCctxWithinRateLimitResponse extends Message
totalPending: bigint;

/**
* @generated from field: bool rate_limit_exceeded = 3;
* @generated from field: uint64 value_within_window = 3;
*/
valueWithinWindow: bigint;

/**
* @generated from field: bool rate_limit_exceeded = 4;
*/
rateLimitExceeded: boolean;

Expand Down
136 changes: 90 additions & 46 deletions x/crosschain/keeper/grpc_query_cctx_rate_limit.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import (
"context"
"sort"
"strings"

sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -68,20 +69,22 @@
}

// calculate the rate limiter sliding window left boundary (inclusive)
leftWindowBoundary := height - rateLimitFlags.Window
leftWindowBoundary := height - rateLimitFlags.Window + 1
if leftWindowBoundary < 0 {
leftWindowBoundary = 0
}

// get the conversion rates for all foreign coins
var gasCoinRates map[int64]sdk.Dec
var erc20CoinRates map[int64]map[string]sdk.Dec
var erc20Coins map[int64]map[string]fungibletypes.ForeignCoins
var rateLimitInZeta sdk.Dec
var foreignCoinMap map[int64]map[string]fungibletypes.ForeignCoins
var windowLimitInZeta sdk.Dec
var blockLimitInZeta sdk.Dec
if applyLimit {
gasCoinRates, erc20CoinRates = k.GetRateLimiterRates(ctx)
erc20Coins = k.fungibleKeeper.GetAllForeignERC20CoinMap(ctx)
rateLimitInZeta = sdk.NewDecFromBigInt(rateLimitFlags.Rate.BigInt())
foreignCoinMap = k.fungibleKeeper.GetAllForeignCoinMap(ctx)
windowLimitInZeta = sdk.NewDecFromBigInt(rateLimitFlags.Rate.BigInt())
blockLimitInZeta = windowLimitInZeta.Quo(sdk.NewDec(rateLimitFlags.Window))
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
}

// the criteria to stop adding cctxs to the rpc response
Expand All @@ -90,15 +93,46 @@
return uint32(len(cctxs)) >= limit
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
}

// query pending nonces for each foreign chain
// if a cctx falls within the rate limiter window
isCctxInWindow := func(cctx *types.CrossChainTx) bool {
// #nosec G701 checked positive
return cctx.InboundTxParams.InboundTxObservedExternalHeight >= uint64(leftWindowBoundary)
}

// query pending nonces for each foreign chain and get the lowest height of the pending cctxs
// Note: The pending nonces could change during the RPC call, so query them beforehand
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
lowestPendingCctxHeight := int64(0)
pendingNoncesMap := make(map[int64]*observertypes.PendingNonces)
for _, chain := range chains {
pendingNonces, found := k.GetObserverKeeper().GetPendingNonces(ctx, tss.TssPubkey, chain.ChainId)
if !found {
return nil, status.Error(codes.Internal, "pending nonces not found")
}
pendingNoncesMap[chain.ChainId] = &pendingNonces

// insert pending nonces and update lowest height
if pendingNonces.NonceLow < pendingNonces.NonceHigh {
pendingNoncesMap[chain.ChainId] = &pendingNonces
cctx, err := getCctxByChainIDAndNonce(k, ctx, tss.TssPubkey, chain.ChainId, pendingNonces.NonceLow)
if err != nil {
return nil, err

Check warning on line 117 in x/crosschain/keeper/grpc_query_cctx_rate_limit.go

View check run for this annotation

Codecov / codecov/patch

x/crosschain/keeper/grpc_query_cctx_rate_limit.go#L117

Added line #L117 was not covered by tests
}
// #nosec G701 len always in range
cctxHeight := int64(cctx.InboundTxParams.InboundTxObservedExternalHeight)
if lowestPendingCctxHeight == 0 || cctxHeight < lowestPendingCctxHeight {
lowestPendingCctxHeight = cctxHeight
}
}
}

// invariant: for period of time > window, the average rate per block cannot exceed `blockLimitInZeta`
pendingCctxsLimitInZeta := windowLimitInZeta
if lowestPendingCctxHeight != 0 {
// `pendingCctxWindow` is the width of [lowestPendingCctxHeight, height] window
// if the window can be wider than the rate limit window, we should adjust the total limit proportionally
pendingCctxWindow := height - lowestPendingCctxHeight + 1
if pendingCctxWindow > rateLimitFlags.Window {
pendingCctxsLimitInZeta = blockLimitInZeta.Mul(sdk.NewDec(pendingCctxWindow))
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
}
}

// query backwards for potential missed pending cctxs for each foreign chain
Expand All @@ -114,23 +148,29 @@
endNonce = 0
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
}

// add the pending nonces to the total pending
// Note: the `totalPending` may not be accurate only if the rate limiter triggers early exit
// `totalPending` is now used for metrics only and it's okay to trade off accuracy for performance
// #nosec G701 always in range
totalPending += uint64(pendingNonces.NonceHigh - pendingNonces.NonceLow)

// query cctx by nonce backwards to the left boundary of the rate limit sliding window
for nonce := startNonce; nonce >= 0; nonce-- {
cctx, err := getCctxByChainIDAndNonce(k, ctx, tss.TssPubkey, chain.ChainId, nonce)
if err != nil {
return nil, err
}
inWindow := isCctxInWindow(cctx)

// We should at least go backwards by 1000 nonces to pick up missed pending cctxs
// We might go even further back if rate limiter is enabled and the endNonce hasn't hit the left window boundary yet
// There are two criteria to stop scanning backwards:
// criteria #1: we'll stop at the left window boundary if the `endNonce` hasn't hit it yet
// #nosec G701 always positive
if nonce < endNonce && cctx.InboundTxParams.InboundTxObservedExternalHeight < uint64(leftWindowBoundary) {
if nonce < endNonce && !inWindow {
break
}
// criteria #2: we should finish the RPC call if the rate limit is exceeded
if rateLimitExceeded(chain.ChainId, cctx, gasCoinRates, erc20CoinRates, erc20Coins, &totalCctxValueInZeta, rateLimitInZeta) {
if inWindow && rateLimitExceeded(chain.ChainId, cctx, gasCoinRates, erc20CoinRates, foreignCoinMap, &totalCctxValueInZeta, windowLimitInZeta) {
limitExceeded = true
break LoopBackwards
}
Expand All @@ -143,12 +183,6 @@
}
}
}

// add the pending nonces to the total pending
// Note: the `totalPending` may not be accurate only if the rate limiter triggers early exit
// `totalPending` is now used for metrics only and it's okay to trade off accuracy for performance
// #nosec G701 always in range
totalPending += uint64(pendingNonces.NonceHigh - pendingNonces.NonceLow)
}

// query forwards for pending cctxs for each foreign chain
Expand All @@ -167,54 +201,50 @@
break LoopForwards
}
// criteria #2: we should finish the RPC call if the rate limit is exceeded
if rateLimitExceeded(chain.ChainId, cctx, gasCoinRates, erc20CoinRates, erc20Coins, &totalCctxValueInZeta, rateLimitInZeta) {
if rateLimitExceeded(chain.ChainId, cctx, gasCoinRates, erc20CoinRates, foreignCoinMap, &totalCctxValueInZeta, pendingCctxsLimitInZeta) {
limitExceeded = true
break LoopForwards
}
cctxs = append(cctxs, cctx)
}
}

// sort the cctxs by chain ID and nonce (lower nonce holds higher priority for scheduling)
sort.Slice(cctxs, func(i, j int) bool {
if cctxs[i].GetCurrentOutTxParam().ReceiverChainId == cctxs[j].GetCurrentOutTxParam().ReceiverChainId {
return cctxs[i].GetCurrentOutTxParam().OutboundTxTssNonce < cctxs[j].GetCurrentOutTxParam().OutboundTxTssNonce
}
return cctxs[i].GetCurrentOutTxParam().ReceiverChainId < cctxs[j].GetCurrentOutTxParam().ReceiverChainId
})

return &types.QueryListPendingCctxWithinRateLimitResponse{
CrossChainTx: cctxs,
TotalPending: totalPending,
ValueWithinWindow: totalCctxValueInZeta.TruncateInt().Uint64(),
RateLimitExceeded: limitExceeded,
}, nil
}

// convertCctxValue converts the value of the cctx in ZETA using given conversion rates
func convertCctxValue(
// ConvertCctxValue converts the value of the cctx in ZETA using given conversion rates
func ConvertCctxValue(
chainID int64,
cctx *types.CrossChainTx,
gasCoinRates map[int64]sdk.Dec,
erc20CoinRates map[int64]map[string]sdk.Dec,
erc20Coins map[int64]map[string]fungibletypes.ForeignCoins,
foreignCoinMap map[int64]map[string]fungibletypes.ForeignCoins,
) sdk.Dec {
var rate sdk.Dec
var decimals uint64
switch cctx.InboundTxParams.CoinType {
case coin.CoinType_Zeta:
// no conversion needed for ZETA
rate = sdk.NewDec(1)
amountCctx := sdk.NewDecFromBigInt(cctx.GetCurrentOutTxParam().Amount.BigInt())
return amountCctx.Quo(sdk.NewDec(10).Power(18))
case coin.CoinType_Gas:
rate = gasCoinRates[chainID]
case coin.CoinType_ERC20:
// get the ERC20 coin decimals
_, found := erc20Coins[chainID]
if !found {
// skip if no coin found for this chainID
return sdk.NewDec(0)
}
fCoin, found := erc20Coins[chainID][strings.ToLower(cctx.InboundTxParams.Asset)]
if !found {
// skip if no coin found for this Asset
return sdk.NewDec(0)
}
// #nosec G701 always in range
decimals = uint64(fCoin.Decimals)

// get the ERC20 coin rate
_, found = erc20CoinRates[chainID]
_, found := erc20CoinRates[chainID]
if !found {
// skip if no rate found for this chainID
return sdk.NewDec(0)
Expand All @@ -224,21 +254,35 @@
// skip CoinType_Cmd
return sdk.NewDec(0)
}

// should not happen, return 0 to skip if it happens
if rate.LTE(sdk.NewDec(0)) {
if rate.IsNil() || rate.LTE(sdk.NewDec(0)) {
return sdk.NewDec(0)
}

// get foreign coin decimals
foreignCoinFromChainMap, found := foreignCoinMap[chainID]
if !found {
// skip if no coin found for this chainID
return sdk.NewDec(0)
}
foreignCoin, found := foreignCoinFromChainMap[strings.ToLower(cctx.InboundTxParams.Asset)]
if !found {
// skip if no coin found for this Asset
return sdk.NewDec(0)
}
decimals = uint64(foreignCoin.Decimals)

// the reciprocal of `rate` is the amount of zrc20 needed to buy 1 ZETA
// for example, given rate = 0.8, the reciprocal is 1.25, which means 1.25 ZRC20 can buy 1 ZETA
// given decimals = 6, the `oneZeta` amount will be 1.25 * 10^6 = 1250000
oneZrc20 := sdk.NewDec(1).Power(decimals)
oneZeta := oneZrc20.Quo(rate)
// given decimals = 6, the `oneZrc20` amount will be 10^6 = 1000000
oneZrc20 := sdk.NewDec(10).Power(decimals)

// convert asset amount into ZETA
// step 1: convert the amount into ZRC20 integer amount
// step 2: convert the ZRC20 integer amount into decimal amount
// given amountCctx = 2000000, rate = 0.8, decimals = 6
// the amountZrc20 = 2000000 * 0.8 = 1600000, the amountZeta = 1600000 / 1000000 = 1.6
amountCctx := sdk.NewDecFromBigInt(cctx.GetCurrentOutTxParam().Amount.BigInt())
amountZeta := amountCctx.Quo(oneZeta)
amountZrc20 := amountCctx.Mul(rate)
amountZeta := amountZrc20.Quo(oneZrc20)
return amountZeta
}

Expand All @@ -249,11 +293,11 @@
cctx *types.CrossChainTx,
gasCoinRates map[int64]sdk.Dec,
erc20CoinRates map[int64]map[string]sdk.Dec,
erc20Coins map[int64]map[string]fungibletypes.ForeignCoins,
foreignCoinMap map[int64]map[string]fungibletypes.ForeignCoins,
currentCctxValue *sdk.Dec,
rateLimitValue sdk.Dec,
) bool {
amountZeta := convertCctxValue(chainID, cctx, gasCoinRates, erc20CoinRates, erc20Coins)
amountZeta := ConvertCctxValue(chainID, cctx, gasCoinRates, erc20CoinRates, foreignCoinMap)
*currentCctxValue = currentCctxValue.Add(amountZeta)
return currentCctxValue.GT(rateLimitValue)
}
Loading
Loading