Skip to content

Commit df872bd

Browse files
mergify[bot]ttl33
andauthored
Fix: deterministically fetch perp info from state (backport #2341) (#2354)
Co-authored-by: ttl33 <19664986+ttl33@users.noreply.github.com>
1 parent 8843936 commit df872bd

File tree

3 files changed

+131
-8
lines changed

3 files changed

+131
-8
lines changed

protocol/testutil/constants/positions.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ var (
7373
PerpetualId: 1,
7474
Quantums: dtypes.NewIntFromBigInt(BigNegMaxUint64()), // 18,446,744,070 ETH, -$55,340,232,210,000 notional.
7575
}
76+
// SOL positions
77+
PerpetualPosition_OneSolLong = satypes.PerpetualPosition{
78+
PerpetualId: 2,
79+
Quantums: dtypes.NewInt(100_000_000_000), // 1 SOL
80+
FundingIndex: dtypes.NewInt(0),
81+
}
7682
// Long position for arbitrary isolated market
7783
PerpetualPosition_OneISOLong = satypes.PerpetualPosition{
7884
PerpetualId: 3,

protocol/x/subaccounts/keeper/subaccount.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,29 +1027,32 @@ func (k Keeper) GetAllRelevantPerpetuals(
10271027
map[uint32]perptypes.PerpInfo,
10281028
error,
10291029
) {
1030-
subaccountIds := make(map[types.SubaccountId]struct{})
1031-
perpIds := make(map[uint32]struct{})
1030+
subaccountIdsSet := make(map[types.SubaccountId]struct{})
1031+
perpIdsSet := make(map[uint32]struct{})
10321032

10331033
// Add all relevant perpetuals in every update.
10341034
for _, update := range updates {
10351035
// If this subaccount has not been processed already, get all of its existing perpetuals.
1036-
if _, exists := subaccountIds[update.SubaccountId]; !exists {
1036+
if _, exists := subaccountIdsSet[update.SubaccountId]; !exists {
10371037
sa := k.GetSubaccount(ctx, update.SubaccountId)
10381038
for _, postition := range sa.PerpetualPositions {
1039-
perpIds[postition.PerpetualId] = struct{}{}
1039+
perpIdsSet[postition.PerpetualId] = struct{}{}
10401040
}
1041-
subaccountIds[update.SubaccountId] = struct{}{}
1041+
subaccountIdsSet[update.SubaccountId] = struct{}{}
10421042
}
10431043

10441044
// Add all perpetuals in the update.
10451045
for _, perpUpdate := range update.PerpetualUpdates {
1046-
perpIds[perpUpdate.GetId()] = struct{}{}
1046+
perpIdsSet[perpUpdate.GetId()] = struct{}{}
10471047
}
10481048
}
10491049

1050+
// Important: Sort the perpIds to ensure determinism!
1051+
sortedPerpIds := lib.GetSortedKeys[lib.Sortable[uint32]](perpIdsSet)
1052+
10501053
// Get all perpetual information from state.
1051-
perpetuals := make(map[uint32]perptypes.PerpInfo, len(perpIds))
1052-
for perpId := range perpIds {
1054+
perpetuals := make(map[uint32]perptypes.PerpInfo, len(sortedPerpIds))
1055+
for _, perpId := range sortedPerpIds {
10531056
perpetual,
10541057
price,
10551058
liquidityTier,

protocol/x/subaccounts/keeper/subaccount_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import (
1010
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
1111
"github.com/dydxprotocol/v4-chain/protocol/lib"
1212

13+
storetypes "cosmossdk.io/store/types"
1314
sdk "github.com/cosmos/cosmos-sdk/types"
1415
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
1516
"github.com/dydxprotocol/v4-chain/protocol/dtypes"
1617
indexerevents "github.com/dydxprotocol/v4-chain/protocol/indexer/events"
1718
bank_testutil "github.com/dydxprotocol/v4-chain/protocol/testutil/bank"
1819
big_testutil "github.com/dydxprotocol/v4-chain/protocol/testutil/big"
1920
"github.com/dydxprotocol/v4-chain/protocol/testutil/constants"
21+
keepertest "github.com/dydxprotocol/v4-chain/protocol/testutil/keeper"
2022
testutil "github.com/dydxprotocol/v4-chain/protocol/testutil/keeper"
2123
"github.com/dydxprotocol/v4-chain/protocol/testutil/nullify"
2224
perptest "github.com/dydxprotocol/v4-chain/protocol/testutil/perpetuals"
@@ -5937,3 +5939,115 @@ func TestIsValidStateTransitionForUndercollateralizedSubaccount_ZeroMarginRequir
59375939
})
59385940
}
59395941
}
5942+
5943+
func TestGetAllRelevantPerpetuals_Deterministic(t *testing.T) {
5944+
tests := map[string]struct {
5945+
// state
5946+
perpetuals []perptypes.Perpetual
5947+
5948+
// subaccount state
5949+
assetPositions []*types.AssetPosition
5950+
perpetualPositions []*types.PerpetualPosition
5951+
5952+
// updates
5953+
assetUpdates []types.AssetUpdate
5954+
perpetualUpdates []types.PerpetualUpdate
5955+
}{
5956+
"Gas used is deterministic when erroring on gas usage": {
5957+
assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(10_000_000_001)), // $10,000.000001
5958+
perpetuals: []perptypes.Perpetual{
5959+
constants.BtcUsd_NoMarginRequirement,
5960+
constants.EthUsd_NoMarginRequirement,
5961+
constants.SolUsd_20PercentInitial_10PercentMaintenance,
5962+
},
5963+
perpetualPositions: []*types.PerpetualPosition{
5964+
&constants.PerpetualPosition_OneBTCLong,
5965+
&constants.PerpetualPosition_OneTenthEthLong,
5966+
&constants.PerpetualPosition_OneSolLong,
5967+
},
5968+
assetUpdates: []types.AssetUpdate{
5969+
{
5970+
AssetId: constants.Usdc.Id,
5971+
BigQuantumsDelta: big.NewInt(1_000_000), // +1 USDC
5972+
},
5973+
},
5974+
perpetualUpdates: []types.PerpetualUpdate{
5975+
{
5976+
PerpetualId: uint32(0),
5977+
BigQuantumsDelta: big.NewInt(-200_000_000), // -2 BTC
5978+
},
5979+
{
5980+
PerpetualId: uint32(1),
5981+
BigQuantumsDelta: big.NewInt(250_000_000), // .25 ETH
5982+
},
5983+
{
5984+
PerpetualId: uint32(2),
5985+
BigQuantumsDelta: big.NewInt(500_000_000), // .005 SOL
5986+
},
5987+
},
5988+
},
5989+
}
5990+
5991+
for name, tc := range tests {
5992+
t.Run(name, func(t *testing.T) {
5993+
// Setup.
5994+
ctx, keeper, pricesKeeper, perpetualsKeeper, _, _, assetsKeeper, _, _ := keepertest.SubaccountsKeepers(
5995+
t,
5996+
true,
5997+
)
5998+
keepertest.CreateTestMarkets(t, ctx, pricesKeeper)
5999+
keepertest.CreateTestLiquidityTiers(t, ctx, perpetualsKeeper)
6000+
keepertest.CreateTestPerpetuals(t, ctx, perpetualsKeeper)
6001+
for _, p := range tc.perpetuals {
6002+
perpetualsKeeper.SetPerpetualForTest(ctx, p)
6003+
}
6004+
require.NoError(t, keepertest.CreateUsdcAsset(ctx, assetsKeeper))
6005+
6006+
subaccount := createNSubaccount(keeper, ctx, 1, big.NewInt(1_000))[0]
6007+
subaccount.PerpetualPositions = tc.perpetualPositions
6008+
subaccount.AssetPositions = tc.assetPositions
6009+
keeper.SetSubaccount(ctx, subaccount)
6010+
subaccountId := *subaccount.Id
6011+
6012+
update := types.Update{
6013+
SubaccountId: subaccountId,
6014+
AssetUpdates: tc.assetUpdates,
6015+
PerpetualUpdates: tc.perpetualUpdates,
6016+
}
6017+
6018+
// Execute.
6019+
gasUsedBefore := ctx.GasMeter().GasConsumed()
6020+
_, err := keeper.GetAllRelevantPerpetuals(ctx, []types.Update{update})
6021+
require.NoError(t, err)
6022+
gasUsedAfter := ctx.GasMeter().GasConsumed()
6023+
6024+
gasUsed := uint64(0)
6025+
// Run 100 times since it's highly unlikely gas usage is deterministic over 100 times if
6026+
// there's non-determinism.
6027+
for range 100 {
6028+
// divide by 2 so that the state read fails at least second to last time.
6029+
ctxWithLimitedGas := ctx.WithGasMeter(storetypes.NewGasMeter((gasUsedAfter - gasUsedBefore) / 2))
6030+
6031+
require.PanicsWithValue(
6032+
t,
6033+
storetypes.ErrorOutOfGas{Descriptor: "ReadPerByte"},
6034+
func() {
6035+
_, _ = keeper.GetAllRelevantPerpetuals(ctxWithLimitedGas, []types.Update{update})
6036+
},
6037+
)
6038+
6039+
if gasUsed == 0 {
6040+
gasUsed = ctxWithLimitedGas.GasMeter().GasConsumed()
6041+
require.Greater(t, gasUsed, uint64(0))
6042+
} else {
6043+
require.Equal(
6044+
t,
6045+
gasUsed,
6046+
ctxWithLimitedGas.GasMeter().GasConsumed(),
6047+
"Gas usage when out of gas is not deterministic",
6048+
)
6049+
}
6050+
}
6051+
})
6052+
}
6053+
}

0 commit comments

Comments
 (0)