Skip to content

Commit 392bbac

Browse files
mergify[bot]ttl33jonfung-dydx
authored
Fix: deterministically fetch perp info from state (backport #2341) (#2345)
Co-authored-by: ttl33 <19664986+ttl33@users.noreply.github.com> Co-authored-by: Jonathan Fung <jonathan@dydx.exchange>
1 parent 6c89c4d commit 392bbac

File tree

3 files changed

+130
-8
lines changed

3 files changed

+130
-8
lines changed

protocol/testutil/constants/positions.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ var (
9999
big.NewInt(0),
100100
big.NewInt(0),
101101
)
102+
// SOL positions
103+
PerpetualPosition_OneSolLong = *testutil.CreateSinglePerpetualPosition(
104+
2,
105+
big.NewInt(100_000_000_000), // 1 SOL
106+
big.NewInt(0),
107+
big.NewInt(0),
108+
)
102109
// Long position for arbitrary isolated market
103110
PerpetualPosition_OneISOLong = *testutil.CreateSinglePerpetualPosition(
104111
3,

protocol/x/subaccounts/keeper/subaccount.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -687,30 +687,33 @@ func (k Keeper) GetAllRelevantPerpetuals(
687687
perptypes.PerpInfos,
688688
error,
689689
) {
690-
subaccountIds := make(map[types.SubaccountId]struct{})
691-
perpIds := make(map[uint32]struct{})
690+
subaccountIdsSet := make(map[types.SubaccountId]struct{})
691+
perpIdsSet := make(map[uint32]struct{})
692692

693693
// Add all relevant perpetuals in every update.
694694
for _, update := range updates {
695695
// If this subaccount has not been processed already, get all of its existing perpetuals.
696-
if _, exists := subaccountIds[update.SubaccountId]; !exists {
696+
if _, exists := subaccountIdsSet[update.SubaccountId]; !exists {
697697
sa := k.GetSubaccount(ctx, update.SubaccountId)
698698
for _, postition := range sa.PerpetualPositions {
699-
perpIds[postition.PerpetualId] = struct{}{}
699+
perpIdsSet[postition.PerpetualId] = struct{}{}
700700
}
701-
subaccountIds[update.SubaccountId] = struct{}{}
701+
subaccountIdsSet[update.SubaccountId] = struct{}{}
702702
}
703703

704704
// Add all perpetuals in the update.
705705
for _, perpUpdate := range update.PerpetualUpdates {
706-
perpIds[perpUpdate.GetId()] = struct{}{}
706+
perpIdsSet[perpUpdate.GetId()] = struct{}{}
707707
}
708708
}
709709

710+
// Important: Sort the perpIds to ensure determinism!
711+
sortedPerpIds := lib.GetSortedKeys[lib.Sortable[uint32]](perpIdsSet)
712+
710713
// Get all perpetual information from state.
711714
ltCache := make(map[uint32]perptypes.LiquidityTier)
712-
perpInfos := make(perptypes.PerpInfos, len(perpIds))
713-
for perpId := range perpIds {
715+
perpInfos := make(perptypes.PerpInfos, len(sortedPerpIds))
716+
for _, perpId := range sortedPerpIds {
714717
perpetual, price, err := k.perpetualsKeeper.GetPerpetualAndMarketPrice(ctx, perpId)
715718
if err != nil {
716719
return nil, err

protocol/x/subaccounts/keeper/subaccount_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ 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"
@@ -6018,3 +6019,114 @@ func TestGetNetCollateralAndMarginRequirements(t *testing.T) {
60186019
})
60196020
}
60206021
}
6022+
6023+
func TestGetAllRelevantPerpetuals_Deterministic(t *testing.T) {
6024+
tests := map[string]struct {
6025+
// state
6026+
perpetuals []perptypes.Perpetual
6027+
6028+
// subaccount state
6029+
assetPositions []*types.AssetPosition
6030+
perpetualPositions []*types.PerpetualPosition
6031+
6032+
// updates
6033+
assetUpdates []types.AssetUpdate
6034+
perpetualUpdates []types.PerpetualUpdate
6035+
}{
6036+
"Gas used is deterministic when erroring on gas usage": {
6037+
assetPositions: testutil.CreateUsdcAssetPositions(big.NewInt(10_000_000_001)), // $10,000.000001
6038+
perpetuals: []perptypes.Perpetual{
6039+
constants.BtcUsd_NoMarginRequirement,
6040+
constants.EthUsd_NoMarginRequirement,
6041+
constants.SolUsd_20PercentInitial_10PercentMaintenance,
6042+
},
6043+
perpetualPositions: []*types.PerpetualPosition{
6044+
&constants.PerpetualPosition_OneBTCLong,
6045+
&constants.PerpetualPosition_OneTenthEthLong,
6046+
&constants.PerpetualPosition_OneSolLong,
6047+
},
6048+
assetUpdates: []types.AssetUpdate{
6049+
{
6050+
AssetId: constants.Usdc.Id,
6051+
BigQuantumsDelta: big.NewInt(1_000_000), // +1 USDC
6052+
},
6053+
},
6054+
perpetualUpdates: []types.PerpetualUpdate{
6055+
{
6056+
PerpetualId: uint32(0),
6057+
BigQuantumsDelta: big.NewInt(-200_000_000), // -2 BTC
6058+
},
6059+
{
6060+
PerpetualId: uint32(1),
6061+
BigQuantumsDelta: big.NewInt(250_000_000), // .25 ETH
6062+
},
6063+
{
6064+
PerpetualId: uint32(2),
6065+
BigQuantumsDelta: big.NewInt(500_000_000), // .005 SOL
6066+
},
6067+
},
6068+
},
6069+
}
6070+
for name, tc := range tests {
6071+
t.Run(name, func(t *testing.T) {
6072+
// Setup.
6073+
ctx, keeper, pricesKeeper, perpetualsKeeper, _, _, assetsKeeper, _, _, _ := keepertest.SubaccountsKeepers(
6074+
t,
6075+
true,
6076+
)
6077+
keepertest.CreateTestMarkets(t, ctx, pricesKeeper)
6078+
keepertest.CreateTestLiquidityTiers(t, ctx, perpetualsKeeper)
6079+
keepertest.CreateTestPerpetuals(t, ctx, perpetualsKeeper)
6080+
for _, p := range tc.perpetuals {
6081+
perpetualsKeeper.SetPerpetualForTest(ctx, p)
6082+
}
6083+
require.NoError(t, keepertest.CreateUsdcAsset(ctx, assetsKeeper))
6084+
6085+
subaccount := createNSubaccount(keeper, ctx, 1, big.NewInt(1_000))[0]
6086+
subaccount.PerpetualPositions = tc.perpetualPositions
6087+
subaccount.AssetPositions = tc.assetPositions
6088+
keeper.SetSubaccount(ctx, subaccount)
6089+
subaccountId := *subaccount.Id
6090+
6091+
update := types.Update{
6092+
SubaccountId: subaccountId,
6093+
AssetUpdates: tc.assetUpdates,
6094+
PerpetualUpdates: tc.perpetualUpdates,
6095+
}
6096+
6097+
// Execute.
6098+
gasUsedBefore := ctx.GasMeter().GasConsumed()
6099+
_, err := keeper.GetAllRelevantPerpetuals(ctx, []types.Update{update})
6100+
require.NoError(t, err)
6101+
gasUsedAfter := ctx.GasMeter().GasConsumed()
6102+
6103+
gasUsed := uint64(0)
6104+
// Run 100 times since it's highly unlikely gas usage is deterministic over 100 times if
6105+
// there's non-determinism.
6106+
for range 100 {
6107+
// divide by 2 so that the state read fails at least second to last time.
6108+
ctxWithLimitedGas := ctx.WithGasMeter(storetypes.NewGasMeter((gasUsedAfter - gasUsedBefore) / 2))
6109+
6110+
require.PanicsWithValue(
6111+
t,
6112+
storetypes.ErrorOutOfGas{Descriptor: "ReadFlat"},
6113+
func() {
6114+
_, _ = keeper.GetAllRelevantPerpetuals(ctxWithLimitedGas, []types.Update{update})
6115+
},
6116+
)
6117+
6118+
if gasUsed == 0 {
6119+
gasUsed = ctxWithLimitedGas.GasMeter().GasConsumed()
6120+
require.Greater(t, gasUsed, uint64(0))
6121+
} else {
6122+
require.Equal(
6123+
t,
6124+
gasUsed,
6125+
ctxWithLimitedGas.GasMeter().GasConsumed(),
6126+
"Gas usage when out of gas is not deterministic",
6127+
)
6128+
}
6129+
}
6130+
})
6131+
}
6132+
}

0 commit comments

Comments
 (0)