Skip to content

Commit cc1b795

Browse files
authored
Fix: deterministically fetch perp info from state (#2341)
1 parent 02d0b34 commit cc1b795

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
@@ -770,30 +770,33 @@ func (k Keeper) GetAllRelevantPerpetuals(
770770
perptypes.PerpInfos,
771771
error,
772772
) {
773-
subaccountIds := make(map[types.SubaccountId]struct{})
774-
perpIds := make(map[uint32]struct{})
773+
subaccountIdsSet := make(map[types.SubaccountId]struct{})
774+
perpIdsSet := make(map[uint32]struct{})
775775

776776
// Add all relevant perpetuals in every update.
777777
for _, update := range updates {
778778
// If this subaccount has not been processed already, get all of its existing perpetuals.
779-
if _, exists := subaccountIds[update.SubaccountId]; !exists {
779+
if _, exists := subaccountIdsSet[update.SubaccountId]; !exists {
780780
sa := k.GetSubaccount(ctx, update.SubaccountId)
781781
for _, postition := range sa.PerpetualPositions {
782-
perpIds[postition.PerpetualId] = struct{}{}
782+
perpIdsSet[postition.PerpetualId] = struct{}{}
783783
}
784-
subaccountIds[update.SubaccountId] = struct{}{}
784+
subaccountIdsSet[update.SubaccountId] = struct{}{}
785785
}
786786

787787
// Add all perpetuals in the update.
788788
for _, perpUpdate := range update.PerpetualUpdates {
789-
perpIds[perpUpdate.GetId()] = struct{}{}
789+
perpIdsSet[perpUpdate.GetId()] = struct{}{}
790790
}
791791
}
792792

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

0 commit comments

Comments
 (0)