diff --git a/protocol/testutil/constants/positions.go b/protocol/testutil/constants/positions.go index 0dcf8e48cc..d46efa00d7 100644 --- a/protocol/testutil/constants/positions.go +++ b/protocol/testutil/constants/positions.go @@ -9,6 +9,7 @@ import ( var ( // Perpetual Positions. +<<<<<<< HEAD Long_Perp_1BTC_PositiveFunding = satypes.PerpetualPosition{ PerpetualId: 0, Quantums: dtypes.NewInt(100_000_000), // 1 BTC @@ -73,6 +74,105 @@ var ( PerpetualId: 1, Quantums: dtypes.NewIntFromBigInt(BigNegMaxUint64()), // 18,446,744,070 ETH, -$55,340,232,210,000 notional. } +======= + Long_Perp_1BTC_PositiveFunding = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(100_000_000), // 1 BTC + big.NewInt(0), + big.NewInt(0), + ) + Short_Perp_1ETH_NegativeFunding = *testutil.CreateSinglePerpetualPosition( + 1, + big.NewInt(-100_000_000), // 1 ETH + big.NewInt(-1), + big.NewInt(0), + ) + PerpetualPosition_OneBTCLong = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(100_000_000), // 1 BTC, $50,000 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_OneBTCShort = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(-100_000_000), // 1 BTC, -$50,000 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_OneTenthBTCLong = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(10_000_000), // 0.1 BTC, $5,000 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_OneTenthBTCShort = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(-10_000_000), // 0.1 BTC, -$5,000 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_OneHundredthBTCLong = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(1_000_000), // 0.01 BTC, $500 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_OneHundredthBTCShort = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(-1_000_000), // 0.01 BTC, -$500 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_FourThousandthsBTCLong = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(400_000), // 0.004 BTC, $200 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_FourThousandthsBTCShort = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(-400_000), // 0.004 BTC, -$200 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_OneAndHalfBTCLong = *testutil.CreateSinglePerpetualPosition( + 0, + big.NewInt(150_000_000), // 1.5 BTC, $75,000 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_OneTenthEthLong = *testutil.CreateSinglePerpetualPosition( + 1, + big.NewInt(100_000_000), // 0.1 ETH, $300 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_OneTenthEthShort = *testutil.CreateSinglePerpetualPosition( + 1, + big.NewInt(-100_000_000), // 0.1 ETH, -$300 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_MaxUint64EthLong = *testutil.CreateSinglePerpetualPosition( + 1, + big.NewInt(0).SetUint64(math.MaxUint64), // 18,446,744,070 ETH, $55,340,232,210,000 notional. + big.NewInt(0), + big.NewInt(0), + ) + PerpetualPosition_MaxUint64EthShort = *testutil.CreateSinglePerpetualPosition( + 1, + BigNegMaxUint64(), // 18,446,744,070 ETH, -$55,340,232,210,000 notional. + big.NewInt(0), + big.NewInt(0), + ) + // SOL positions + PerpetualPosition_OneSolLong = *testutil.CreateSinglePerpetualPosition( + 2, + big.NewInt(100_000_000_000), // 1 SOL + big.NewInt(0), + big.NewInt(0), + ) +>>>>>>> cc1b7954 (Fix: deterministically fetch perp info from state (#2341)) // Long position for arbitrary isolated market PerpetualPosition_OneISOLong = satypes.PerpetualPosition{ PerpetualId: 3, diff --git a/protocol/x/subaccounts/keeper/subaccount.go b/protocol/x/subaccounts/keeper/subaccount.go index 5a10c242a0..2af4014b19 100644 --- a/protocol/x/subaccounts/keeper/subaccount.go +++ b/protocol/x/subaccounts/keeper/subaccount.go @@ -1027,33 +1027,43 @@ func (k Keeper) GetAllRelevantPerpetuals( map[uint32]perptypes.PerpInfo, error, ) { - subaccountIds := make(map[types.SubaccountId]struct{}) - perpIds := make(map[uint32]struct{}) + subaccountIdsSet := make(map[types.SubaccountId]struct{}) + perpIdsSet := make(map[uint32]struct{}) // Add all relevant perpetuals in every update. for _, update := range updates { // If this subaccount has not been processed already, get all of its existing perpetuals. - if _, exists := subaccountIds[update.SubaccountId]; !exists { + if _, exists := subaccountIdsSet[update.SubaccountId]; !exists { sa := k.GetSubaccount(ctx, update.SubaccountId) for _, postition := range sa.PerpetualPositions { - perpIds[postition.PerpetualId] = struct{}{} + perpIdsSet[postition.PerpetualId] = struct{}{} } - subaccountIds[update.SubaccountId] = struct{}{} + subaccountIdsSet[update.SubaccountId] = struct{}{} } // Add all perpetuals in the update. for _, perpUpdate := range update.PerpetualUpdates { - perpIds[perpUpdate.GetId()] = struct{}{} + perpIdsSet[perpUpdate.GetId()] = struct{}{} } } + // Important: Sort the perpIds to ensure determinism! + sortedPerpIds := lib.GetSortedKeys[lib.Sortable[uint32]](perpIdsSet) + // Get all perpetual information from state. +<<<<<<< HEAD perpetuals := make(map[uint32]perptypes.PerpInfo, len(perpIds)) for perpId := range perpIds { perpetual, price, liquidityTier, err := k.perpetualsKeeper.GetPerpetualAndMarketPriceAndLiquidityTier(ctx, perpId) +======= + ltCache := make(map[uint32]perptypes.LiquidityTier) + perpInfos := make(perptypes.PerpInfos, len(sortedPerpIds)) + for _, perpId := range sortedPerpIds { + perpetual, price, err := k.perpetualsKeeper.GetPerpetualAndMarketPrice(ctx, perpId) +>>>>>>> cc1b7954 (Fix: deterministically fetch perp info from state (#2341)) if err != nil { return nil, err } diff --git a/protocol/x/subaccounts/keeper/subaccount_test.go b/protocol/x/subaccounts/keeper/subaccount_test.go index 747ff8cfb9..3dae58010a 100644 --- a/protocol/x/subaccounts/keeper/subaccount_test.go +++ b/protocol/x/subaccounts/keeper/subaccount_test.go @@ -10,6 +10,7 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/dydxprotocol/v4-chain/protocol/lib" + storetypes "cosmossdk.io/store/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/dydxprotocol/v4-chain/protocol/dtypes" @@ -5850,6 +5851,7 @@ func TestGetNetCollateralAndMarginRequirements(t *testing.T) { } } +<<<<<<< HEAD func TestIsValidStateTransitionForUndercollateralizedSubaccount_ZeroMarginRequirements(t *testing.T) { tests := map[string]struct { bigCurNetCollateral *big.Int @@ -5934,6 +5936,115 @@ func TestIsValidStateTransitionForUndercollateralizedSubaccount_ZeroMarginRequir tc.bigNewMaintenanceMargin, ), ) +======= +func TestGetAllRelevantPerpetuals_Deterministic(t *testing.T) { + tests := map[string]struct { + // state + perpetuals []perptypes.Perpetual + + // subaccount state + assetPositions []*types.AssetPosition + perpetualPositions []*types.PerpetualPosition + + // updates + assetUpdates []types.AssetUpdate + perpetualUpdates []types.PerpetualUpdate + }{ + "Gas used is deterministic when erroring on gas usage": { + assetPositions: testutil.CreateUsdcAssetPositions(big.NewInt(10_000_000_001)), // $10,000.000001 + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_NoMarginRequirement, + constants.EthUsd_NoMarginRequirement, + constants.SolUsd_20PercentInitial_10PercentMaintenance, + }, + perpetualPositions: []*types.PerpetualPosition{ + &constants.PerpetualPosition_OneBTCLong, + &constants.PerpetualPosition_OneTenthEthLong, + &constants.PerpetualPosition_OneSolLong, + }, + assetUpdates: []types.AssetUpdate{ + { + AssetId: constants.Usdc.Id, + BigQuantumsDelta: big.NewInt(1_000_000), // +1 USDC + }, + }, + perpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(-200_000_000), // -2 BTC + }, + { + PerpetualId: uint32(1), + BigQuantumsDelta: big.NewInt(250_000_000), // .25 ETH + }, + { + PerpetualId: uint32(2), + BigQuantumsDelta: big.NewInt(500_000_000), // .005 SOL + }, + }, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // Setup. + ctx, keeper, pricesKeeper, perpetualsKeeper, _, _, assetsKeeper, _, _, _, _ := keepertest.SubaccountsKeepers( + t, + true, + ) + keepertest.CreateTestMarkets(t, ctx, pricesKeeper) + keepertest.CreateTestLiquidityTiers(t, ctx, perpetualsKeeper) + keepertest.CreateTestPerpetuals(t, ctx, perpetualsKeeper) + for _, p := range tc.perpetuals { + perpetualsKeeper.SetPerpetualForTest(ctx, p) + } + require.NoError(t, keepertest.CreateUsdcAsset(ctx, assetsKeeper)) + + subaccount := createNSubaccount(keeper, ctx, 1, big.NewInt(1_000))[0] + subaccount.PerpetualPositions = tc.perpetualPositions + subaccount.AssetPositions = tc.assetPositions + keeper.SetSubaccount(ctx, subaccount) + subaccountId := *subaccount.Id + + update := types.Update{ + SubaccountId: subaccountId, + AssetUpdates: tc.assetUpdates, + PerpetualUpdates: tc.perpetualUpdates, + } + + // Execute. + gasUsedBefore := ctx.GasMeter().GasConsumed() + _, err := keeper.GetAllRelevantPerpetuals(ctx, []types.Update{update}) + require.NoError(t, err) + gasUsedAfter := ctx.GasMeter().GasConsumed() + + gasUsed := uint64(0) + // Run 100 times since it's highly unlikely gas usage is deterministic over 100 times if + // there's non-determinism. + for range 100 { + // divide by 2 so that the state read fails at least second to last time. + ctxWithLimitedGas := ctx.WithGasMeter(storetypes.NewGasMeter((gasUsedAfter - gasUsedBefore) / 2)) + + require.PanicsWithValue( + t, + storetypes.ErrorOutOfGas{Descriptor: "ReadFlat"}, + func() { + _, _ = keeper.GetAllRelevantPerpetuals(ctxWithLimitedGas, []types.Update{update}) + }, + ) + + if gasUsed == 0 { + gasUsed = ctxWithLimitedGas.GasMeter().GasConsumed() + require.Greater(t, gasUsed, uint64(0)) + } else { + require.Equal( + t, + gasUsed, + ctxWithLimitedGas.GasMeter().GasConsumed(), + "Gas usage when out of gas is not deterministic", + ) + } + } +>>>>>>> cc1b7954 (Fix: deterministically fetch perp info from state (#2341)) }) } }