diff --git a/.github/workflows/protocol-build-and-push-mainnet.yml b/.github/workflows/protocol-build-and-push-mainnet.yml index 3999f9fda4..8ae3bf30cc 100644 --- a/.github/workflows/protocol-build-and-push-mainnet.yml +++ b/.github/workflows/protocol-build-and-push-mainnet.yml @@ -9,6 +9,7 @@ on: # yamllint disable-line rule:truthy branches: - 'release/protocol/v[0-9]+.[0-9]+.x' # e.g. release/protocol/v0.1.x - 'release/protocol/v[0-9]+.x' # e.g. release/protocol/v1.x + - 'td/*' jobs: build-and-push-mainnet: diff --git a/.github/workflows/protocol-build-and-push-testnet.yml b/.github/workflows/protocol-build-and-push-testnet.yml index 29e79317ee..04f949644f 100644 --- a/.github/workflows/protocol-build-and-push-testnet.yml +++ b/.github/workflows/protocol-build-and-push-testnet.yml @@ -9,6 +9,7 @@ on: # yamllint disable-line rule:truthy branches: - 'release/protocol/v[0-9]+.[0-9]+.x' # e.g. release/protocol/v0.1.x - 'release/protocol/v[0-9]+.x' # e.g. release/protocol/v1.x + - 'td/*' jobs: build-and-push-testnet: diff --git a/protocol/app/app.go b/protocol/app/app.go index 5787ff7af3..ab77c825dc 100644 --- a/protocol/app/app.go +++ b/protocol/app/app.go @@ -382,6 +382,12 @@ func New( interfaceRegistry := encodingConfig.InterfaceRegistry txConfig := encodingConfig.TxConfig + // Enable optimistic block execution. + if appFlags.OptimisticExecutionEnabled { + logger.Info("optimistic execution is enabled.") + baseAppOptions = append(baseAppOptions, baseapp.SetOptimisticExecution()) + } + bApp := baseapp.NewBaseApp(appconstants.AppName, logger, db, txConfig.TxDecoder(), baseAppOptions...) bApp.SetCommitMultiStoreTracer(traceStore) bApp.SetVersion(version.Version) @@ -1654,8 +1660,6 @@ func (app *App) EndBlocker(ctx sdk.Context) (sdk.EndBlock, error) { if err != nil { return response, err } - block := app.IndexerEventManager.ProduceBlock(ctx) - app.IndexerEventManager.SendOnchainData(block) return response, err } @@ -1664,6 +1668,8 @@ func (app *App) Precommitter(ctx sdk.Context) { if err := app.ModuleManager.Precommit(ctx); err != nil { panic(err) } + block := app.IndexerEventManager.ProduceBlock(ctx) + app.IndexerEventManager.SendOnchainData(block) } // PrepareCheckStater application updates after commit and before any check state is invoked. diff --git a/protocol/app/flags/flags.go b/protocol/app/flags/flags.go index 5756b69162..bdbe34a514 100644 --- a/protocol/app/flags/flags.go +++ b/protocol/app/flags/flags.go @@ -27,6 +27,8 @@ type Flags struct { GrpcStreamingMaxChannelBufferSize uint32 VEOracleEnabled bool // Slinky Vote Extensions + // Optimistic block execution + OptimisticExecutionEnabled bool } // List of CLI flags. @@ -48,6 +50,9 @@ const ( // Slinky VEs enabled VEOracleEnabled = "slinky-vote-extension-oracle-enabled" + + // Enable optimistic block execution. + OptimisticExecutionEnabled = "optimistic-execution-enabled" ) // Default values. @@ -62,7 +67,8 @@ const ( DefaultGrpcStreamingMaxBatchSize = 2000 DefaultGrpcStreamingMaxChannelBufferSize = 2000 - DefaultVEOracleEnabled = true + DefaultVEOracleEnabled = true + DefaultOptimisticExecutionEnabled = false ) // AddFlagsToCmd adds flags to app initialization. @@ -116,6 +122,11 @@ func AddFlagsToCmd(cmd *cobra.Command) { DefaultVEOracleEnabled, "Whether to run on-chain oracle via slinky vote extensions", ) + cmd.Flags().Bool( + OptimisticExecutionEnabled, + DefaultOptimisticExecutionEnabled, + "Whether to enable optimistic block execution", + ) } // Validate checks that the flags are valid. @@ -127,6 +138,10 @@ func (f *Flags) Validate() error { // Grpc streaming if f.GrpcStreamingEnabled { + if f.OptimisticExecutionEnabled { + // TODO(OTE-456): Finish gRPC streaming x OE integration. + return fmt.Errorf("grpc streaming cannot be enabled together with optimistic execution") + } if !f.GrpcEnable { return fmt.Errorf("grpc.enable must be set to true - grpc streaming requires gRPC server") } @@ -164,7 +179,8 @@ func GetFlagValuesFromOptions( GrpcStreamingMaxBatchSize: DefaultGrpcStreamingMaxBatchSize, GrpcStreamingMaxChannelBufferSize: DefaultGrpcStreamingMaxChannelBufferSize, - VEOracleEnabled: true, + VEOracleEnabled: true, + OptimisticExecutionEnabled: DefaultOptimisticExecutionEnabled, } // Populate the flags if they exist. @@ -234,5 +250,10 @@ func GetFlagValuesFromOptions( } } + if option := appOpts.Get(OptimisticExecutionEnabled); option != nil { + if v, err := cast.ToBoolE(option); err == nil { + result.OptimisticExecutionEnabled = v + } + } return result } diff --git a/protocol/app/flags/flags_test.go b/protocol/app/flags/flags_test.go index 4b5da76819..8260efe313 100644 --- a/protocol/app/flags/flags_test.go +++ b/protocol/app/flags/flags_test.go @@ -41,6 +41,9 @@ func TestAddFlagsToCommand(t *testing.T) { fmt.Sprintf("Has %s flag", flags.GrpcStreamingMaxChannelBufferSize): { flagName: flags.GrpcStreamingMaxChannelBufferSize, }, + fmt.Sprintf("Has %s flag", flags.OptimisticExecutionEnabled): { + flagName: flags.OptimisticExecutionEnabled, + }, } for name, tc := range tests { @@ -57,11 +60,12 @@ func TestValidate(t *testing.T) { }{ "success (default values)": { flags: flags.Flags{ - NonValidatingFullNode: flags.DefaultNonValidatingFullNode, - DdAgentHost: flags.DefaultDdAgentHost, - DdTraceAgentPort: flags.DefaultDdTraceAgentPort, - GrpcAddress: config.DefaultGRPCAddress, - GrpcEnable: true, + NonValidatingFullNode: flags.DefaultNonValidatingFullNode, + DdAgentHost: flags.DefaultDdAgentHost, + DdTraceAgentPort: flags.DefaultDdTraceAgentPort, + GrpcAddress: config.DefaultGRPCAddress, + GrpcEnable: true, + OptimisticExecutionEnabled: false, }, }, "success - full node & gRPC disabled": { @@ -80,6 +84,22 @@ func TestValidate(t *testing.T) { GrpcStreamingMaxChannelBufferSize: 2000, }, }, + "success - optimistic execution": { + flags: flags.Flags{ + NonValidatingFullNode: false, + GrpcEnable: true, + OptimisticExecutionEnabled: true, + }, + }, + "failure - optimistic execution cannot be enabled with gRPC streaming": { + flags: flags.Flags{ + NonValidatingFullNode: false, + GrpcEnable: true, + GrpcStreamingEnabled: true, + OptimisticExecutionEnabled: true, + }, + expectedErr: fmt.Errorf("grpc streaming cannot be enabled together with optimistic execution"), + }, "failure - gRPC disabled": { flags: flags.Flags{ GrpcEnable: false, @@ -153,6 +173,7 @@ func TestGetFlagValuesFromOptions(t *testing.T) { expectedGrpcStreamingFlushMs uint32 expectedGrpcStreamingBatchSize uint32 expectedGrpcStreamingMaxChannelBufferSize uint32 + expectedOptimisticExecutionEnabled bool }{ "Sets to default if unset": { expectedNonValidatingFullNodeFlag: false, @@ -164,6 +185,7 @@ func TestGetFlagValuesFromOptions(t *testing.T) { expectedGrpcStreamingFlushMs: 50, expectedGrpcStreamingBatchSize: 2000, expectedGrpcStreamingMaxChannelBufferSize: 2000, + expectedOptimisticExecutionEnabled: false, }, "Sets values from options": { optsMap: map[string]any{ @@ -176,6 +198,7 @@ func TestGetFlagValuesFromOptions(t *testing.T) { flags.GrpcStreamingFlushIntervalMs: uint32(408), flags.GrpcStreamingMaxBatchSize: uint32(650), flags.GrpcStreamingMaxChannelBufferSize: uint32(972), + flags.OptimisticExecutionEnabled: "true", }, expectedNonValidatingFullNodeFlag: true, expectedDdAgentHost: "agentHostTest", @@ -186,6 +209,7 @@ func TestGetFlagValuesFromOptions(t *testing.T) { expectedGrpcStreamingFlushMs: 408, expectedGrpcStreamingBatchSize: 650, expectedGrpcStreamingMaxChannelBufferSize: 972, + expectedOptimisticExecutionEnabled: true, }, } diff --git a/protocol/go.mod b/protocol/go.mod index 731507414e..57abe428d1 100644 --- a/protocol/go.mod +++ b/protocol/go.mod @@ -451,7 +451,7 @@ replace ( // Use dYdX fork of CometBFT github.com/cometbft/cometbft => github.com/dydxprotocol/cometbft v0.38.6-0.20240426214049-c8beeeada40a // Use dYdX fork of Cosmos SDK - github.com/cosmos/cosmos-sdk => github.com/dydxprotocol/cosmos-sdk v0.50.6-0.20240606183841-18966898625f + github.com/cosmos/cosmos-sdk => github.com/dydxprotocol/cosmos-sdk v0.50.6-0.20240807220600-7a2c45589d7c github.com/cosmos/iavl => github.com/dydxprotocol/iavl v1.1.1-0.20240509161911-1c8b8e787e85 ) diff --git a/protocol/go.sum b/protocol/go.sum index 1fdc736226..670a867e90 100644 --- a/protocol/go.sum +++ b/protocol/go.sum @@ -947,8 +947,8 @@ github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/dydxprotocol/cometbft v0.38.6-0.20240426214049-c8beeeada40a h1:KeQZZsYJQ/AKQ6lbP5lL3N/6aOclxxYG8pIm8zerwZw= github.com/dydxprotocol/cometbft v0.38.6-0.20240426214049-c8beeeada40a/go.mod h1:EBEod7kZfNr4W0VooOrTEMSiXNrSyiQ/M2FL/rOcPCs= -github.com/dydxprotocol/cosmos-sdk v0.50.6-0.20240606183841-18966898625f h1:OLmCAiUdKbz9KNYzNtrvl+Y8l5sNWeWqjN5U5h78ckw= -github.com/dydxprotocol/cosmos-sdk v0.50.6-0.20240606183841-18966898625f/go.mod h1:NruCXox2SOkVq2ZC5Bs8s2sT+jjVqBEU59wXyZOkCkM= +github.com/dydxprotocol/cosmos-sdk v0.50.6-0.20240807220600-7a2c45589d7c h1:xVjHUTNwabuapovByZ13Ei52pJLBhBTdWJHlm3bYEQo= +github.com/dydxprotocol/cosmos-sdk v0.50.6-0.20240807220600-7a2c45589d7c/go.mod h1:NruCXox2SOkVq2ZC5Bs8s2sT+jjVqBEU59wXyZOkCkM= github.com/dydxprotocol/cosmos-sdk/store v1.0.3-0.20240326192503-dd116391188d h1:HgLu1FD2oDFzlKW6/+SFXlH5Os8cwNTbplQIrQOWx8w= github.com/dydxprotocol/cosmos-sdk/store v1.0.3-0.20240326192503-dd116391188d/go.mod h1:zMcD3hfNwd0WMTpdRUhS3QxoCoEtBXWeoKsu3iaLBbQ= github.com/dydxprotocol/iavl v1.1.1-0.20240509161911-1c8b8e787e85 h1:5B/yGZyTBX/OZASQQMnk6Ms/TZja56MYd8OBaVc0Mho= diff --git a/protocol/mocks/ClobKeeper.go b/protocol/mocks/ClobKeeper.go index 0a16ce55b8..28b4e02a10 100644 --- a/protocol/mocks/ClobKeeper.go +++ b/protocol/mocks/ClobKeeper.go @@ -1012,7 +1012,7 @@ func (_m *ClobKeeper) ProcessProposerOperations(ctx types.Context, operations [] } // ProcessSingleMatch provides a mock function with given fields: ctx, matchWithOrders -func (_m *ClobKeeper) ProcessSingleMatch(ctx types.Context, matchWithOrders *clobtypes.MatchWithOrders) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, *clobtypes.OffchainUpdates, error) { +func (_m *ClobKeeper) ProcessSingleMatch(ctx types.Context, matchWithOrders *clobtypes.MatchWithOrders) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, error) { ret := _m.Called(ctx, matchWithOrders) if len(ret) == 0 { @@ -1022,9 +1022,8 @@ func (_m *ClobKeeper) ProcessSingleMatch(ctx types.Context, matchWithOrders *clo var r0 bool var r1 subaccountstypes.UpdateResult var r2 subaccountstypes.UpdateResult - var r3 *clobtypes.OffchainUpdates - var r4 error - if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MatchWithOrders) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, *clobtypes.OffchainUpdates, error)); ok { + var r3 error + if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MatchWithOrders) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, error)); ok { return rf(ctx, matchWithOrders) } if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MatchWithOrders) bool); ok { @@ -1045,21 +1044,13 @@ func (_m *ClobKeeper) ProcessSingleMatch(ctx types.Context, matchWithOrders *clo r2 = ret.Get(2).(subaccountstypes.UpdateResult) } - if rf, ok := ret.Get(3).(func(types.Context, *clobtypes.MatchWithOrders) *clobtypes.OffchainUpdates); ok { + if rf, ok := ret.Get(3).(func(types.Context, *clobtypes.MatchWithOrders) error); ok { r3 = rf(ctx, matchWithOrders) } else { - if ret.Get(3) != nil { - r3 = ret.Get(3).(*clobtypes.OffchainUpdates) - } - } - - if rf, ok := ret.Get(4).(func(types.Context, *clobtypes.MatchWithOrders) error); ok { - r4 = rf(ctx, matchWithOrders) - } else { - r4 = ret.Error(4) + r3 = ret.Error(3) } - return r0, r1, r2, r3, r4 + return r0, r1, r2, r3 } // PruneStateFillAmountsForShortTermOrders provides a mock function with given fields: ctx diff --git a/protocol/mocks/MemClobKeeper.go b/protocol/mocks/MemClobKeeper.go index b7f71ecdb2..166f3b7771 100644 --- a/protocol/mocks/MemClobKeeper.go +++ b/protocol/mocks/MemClobKeeper.go @@ -321,7 +321,7 @@ func (_m *MemClobKeeper) OffsetSubaccountPerpetualPosition(ctx types.Context, li } // ProcessSingleMatch provides a mock function with given fields: ctx, matchWithOrders -func (_m *MemClobKeeper) ProcessSingleMatch(ctx types.Context, matchWithOrders *clobtypes.MatchWithOrders) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, *clobtypes.OffchainUpdates, error) { +func (_m *MemClobKeeper) ProcessSingleMatch(ctx types.Context, matchWithOrders *clobtypes.MatchWithOrders) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, error) { ret := _m.Called(ctx, matchWithOrders) if len(ret) == 0 { @@ -331,9 +331,8 @@ func (_m *MemClobKeeper) ProcessSingleMatch(ctx types.Context, matchWithOrders * var r0 bool var r1 subaccountstypes.UpdateResult var r2 subaccountstypes.UpdateResult - var r3 *clobtypes.OffchainUpdates - var r4 error - if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MatchWithOrders) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, *clobtypes.OffchainUpdates, error)); ok { + var r3 error + if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MatchWithOrders) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, error)); ok { return rf(ctx, matchWithOrders) } if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MatchWithOrders) bool); ok { @@ -354,21 +353,13 @@ func (_m *MemClobKeeper) ProcessSingleMatch(ctx types.Context, matchWithOrders * r2 = ret.Get(2).(subaccountstypes.UpdateResult) } - if rf, ok := ret.Get(3).(func(types.Context, *clobtypes.MatchWithOrders) *clobtypes.OffchainUpdates); ok { + if rf, ok := ret.Get(3).(func(types.Context, *clobtypes.MatchWithOrders) error); ok { r3 = rf(ctx, matchWithOrders) } else { - if ret.Get(3) != nil { - r3 = ret.Get(3).(*clobtypes.OffchainUpdates) - } - } - - if rf, ok := ret.Get(4).(func(types.Context, *clobtypes.MatchWithOrders) error); ok { - r4 = rf(ctx, matchWithOrders) - } else { - r4 = ret.Error(4) + r3 = ret.Error(3) } - return r0, r1, r2, r3, r4 + return r0, r1, r2, r3 } // ReplayPlaceOrder provides a mock function with given fields: ctx, msg diff --git a/protocol/testutil/constants/stateful_orders.go b/protocol/testutil/constants/stateful_orders.go index 58483bbd06..448c846f15 100644 --- a/protocol/testutil/constants/stateful_orders.go +++ b/protocol/testutil/constants/stateful_orders.go @@ -456,6 +456,20 @@ var ( ConditionType: clobtypes.Order_CONDITION_TYPE_STOP_LOSS, ConditionalOrderTriggerSubticks: 20, } + ConditionalOrder_Alice_Num1_Id0_Clob0_Buy5_Price20_GTBT15_StopLoss20 = clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: Alice_Num1, + ClientId: 0, + OrderFlags: clobtypes.OrderIdFlags_Conditional, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 5, + Subticks: 20, + GoodTilOneof: &clobtypes.Order_GoodTilBlockTime{GoodTilBlockTime: 15}, + ConditionType: clobtypes.Order_CONDITION_TYPE_STOP_LOSS, + ConditionalOrderTriggerSubticks: 20, + } ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_TakeProfit20 = clobtypes.Order{ OrderId: clobtypes.OrderId{ SubaccountId: Alice_Num0, @@ -554,6 +568,20 @@ var ( ConditionType: clobtypes.Order_CONDITION_TYPE_STOP_LOSS, ConditionalOrderTriggerSubticks: 25, } + ConditionalOrder_Alice_Num1_Id1_Clob0_Buy15_Price25_GTBT15_StopLoss25 = clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: Alice_Num1, + ClientId: 1, + OrderFlags: clobtypes.OrderIdFlags_Conditional, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 15, + Subticks: 25, + GoodTilOneof: &clobtypes.Order_GoodTilBlockTime{GoodTilBlockTime: 15}, + ConditionType: clobtypes.Order_CONDITION_TYPE_STOP_LOSS, + ConditionalOrderTriggerSubticks: 25, + } ConditionalOrder_Alice_Num0_Id1_Clob0_Buy15_Price10_GTBT15_TakeProfit20 = clobtypes.Order{ OrderId: clobtypes.OrderId{ SubaccountId: Alice_Num0, @@ -624,6 +652,20 @@ var ( ConditionType: clobtypes.Order_CONDITION_TYPE_TAKE_PROFIT, ConditionalOrderTriggerSubticks: 20, } + ConditionalOrder_Alice_Num1_Id2_Clob0_Sell20_Price20_GTBT15_TakeProfit20 = clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: Alice_Num1, + ClientId: 2, + OrderFlags: clobtypes.OrderIdFlags_Conditional, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 20, + Subticks: 20, + GoodTilOneof: &clobtypes.Order_GoodTilBlockTime{GoodTilBlockTime: 15}, + ConditionType: clobtypes.Order_CONDITION_TYPE_TAKE_PROFIT, + ConditionalOrderTriggerSubticks: 20, + } ConditionalOrder_Alice_Num0_Id2_Clob0_Buy20_Price10_GTBT15_TakeProfit10 = clobtypes.Order{ OrderId: clobtypes.OrderId{ SubaccountId: Alice_Num0, @@ -666,6 +708,20 @@ var ( ConditionType: clobtypes.Order_CONDITION_TYPE_STOP_LOSS, ConditionalOrderTriggerSubticks: 25, } + ConditionalOrder_Alice_Num1_Id3_Clob0_Buy25_Price25_GTBT15_StopLoss25 = clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: Alice_Num1, + ClientId: 3, + OrderFlags: clobtypes.OrderIdFlags_Conditional, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 25, + Subticks: 10, + GoodTilOneof: &clobtypes.Order_GoodTilBlockTime{GoodTilBlockTime: 15}, + ConditionType: clobtypes.Order_CONDITION_TYPE_STOP_LOSS, + ConditionalOrderTriggerSubticks: 25, + } ConditionalOrder_Alice_Num0_Id3_Clob0_Buy25_Price10_GTBT15_TakeProfit20 = clobtypes.Order{ OrderId: clobtypes.OrderId{ SubaccountId: Alice_Num0, @@ -722,6 +778,20 @@ var ( ConditionType: clobtypes.Order_CONDITION_TYPE_STOP_LOSS, ConditionalOrderTriggerSubticks: 20, } + ConditionalOrder_Alice_Num0_Id1_Clob1_Buy5_Price10_GTBT15_StopLoss20 = clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: Alice_Num0, + ClientId: 0, + OrderFlags: clobtypes.OrderIdFlags_Conditional, + ClobPairId: 1, + }, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 5, + Subticks: 10, + GoodTilOneof: &clobtypes.Order_GoodTilBlockTime{GoodTilBlockTime: 15}, + ConditionType: clobtypes.Order_CONDITION_TYPE_STOP_LOSS, + ConditionalOrderTriggerSubticks: 20, + } ConditionalOrder_Alice_Num0_Id0_Clob1_Buy5_Price10_GTBT15_TakeProfit20 = clobtypes.Order{ OrderId: clobtypes.OrderId{ SubaccountId: Alice_Num0, diff --git a/protocol/testutil/memclob/keeper.go b/protocol/testutil/memclob/keeper.go index 9ca05d4a96..6410307b54 100644 --- a/protocol/testutil/memclob/keeper.go +++ b/protocol/testutil/memclob/keeper.go @@ -331,7 +331,6 @@ func (f *FakeMemClobKeeper) ProcessSingleMatch( success bool, takerUpdateResult satypes.UpdateResult, makerUpdateResult satypes.UpdateResult, - offchainUpdates *types.OffchainUpdates, err error, ) { makerOrder := matchWithOrders.MakerOrder @@ -364,7 +363,7 @@ func (f *FakeMemClobKeeper) ProcessSingleMatch( ) } - return true, satypes.Success, satypes.Success, types.NewOffchainUpdates(), nil + return true, satypes.Success, satypes.Success, nil } subaccountMatchedOrders := make(map[satypes.SubaccountId][]types.PendingOpenOrder) @@ -411,7 +410,7 @@ func (f *FakeMemClobKeeper) ProcessSingleMatch( } } - return success, takerUpdateResult, makerUpdateResult, types.NewOffchainUpdates(), nil + return success, takerUpdateResult, makerUpdateResult, nil } func (f *FakeMemClobKeeper) GetStatePosition( diff --git a/protocol/x/clob/abci.go b/protocol/x/clob/abci.go index a8c5d34f1b..4fe3304276 100644 --- a/protocol/x/clob/abci.go +++ b/protocol/x/clob/abci.go @@ -89,30 +89,10 @@ func EndBlocker( ) } - deliveredCancels := keeper.GetDeliveredCancelledOrderIds(ctx) - // Prune expired untriggered conditional orders from the in-memory UntriggeredConditionalOrders struct. - keeper.PruneUntriggeredConditionalOrders( - expiredStatefulOrderIds, - deliveredCancels, - ) - // Update the memstore with expired order ids. // These expired stateful order ids will be purged from the memclob in `Commit`. processProposerMatchesEvents.ExpiredStatefulOrderIds = expiredStatefulOrderIds - // Before triggering conditional orders, add newly-placed conditional orders to the clob keeper's - // in-memory UntriggeredConditionalOrders data structure to allow conditional orders to - // trigger in the same block they are placed. Skip triggering orders which have been cancelled - // or expired. - // TODO(CLOB-773) Support conditional order replacements. Ensure replacements are de-duplicated. - conditionalOrdersIds := keeper.GetDeliveredConditionalOrderIds(ctx) - keeper.AddUntriggeredConditionalOrders( - ctx, - conditionalOrdersIds, - lib.UniqueSliceToSet(deliveredCancels), - lib.UniqueSliceToSet(expiredStatefulOrderIds), - ) - // Poll out all triggered conditional orders from `UntriggeredConditionalOrders` and update state. triggeredConditionalOrderIds := keeper.MaybeTriggerConditionalOrders(ctx) // Update the memstore with conditional order ids triggered in the last block. @@ -125,9 +105,6 @@ func EndBlocker( processProposerMatchesEvents, ) - // Prune any rate limiting information that is no longer relevant. - keeper.PruneRateLimits(ctx) - // Emit relevant metrics at the end of every block. metrics.SetGauge( metrics.InsuranceFundBalance, @@ -146,6 +123,9 @@ func PrepareCheckState( log.BlockHeight, ctx.BlockHeight()+1, ) + // Prune any rate limiting information that is no longer relevant. + keeper.PruneRateLimits(ctx) + // Get the events generated from processing the matches in the latest block. processProposerMatchesEvents := keeper.GetProcessProposerMatchesEvents(ctx) if ctx.BlockHeight() != int64(processProposerMatchesEvents.BlockHeight) { diff --git a/protocol/x/clob/abci_test.go b/protocol/x/clob/abci_test.go index 8441f1126b..7f178facb5 100644 --- a/protocol/x/clob/abci_test.go +++ b/protocol/x/clob/abci_test.go @@ -64,94 +64,6 @@ func assertFillAmountAndPruneState( } } -func TestEndBlocker_Failure(t *testing.T) { - blockHeight := uint32(5) - tests := map[string]struct { - blockTime time.Time - expiredStatefulOrderIds []types.OrderId - setupState func(ctx sdk.Context, k keepertest.ClobKeepersTestContext, m *mocks.MemClob) - - expectedPanicMessage string - }{ - "Panics if cancelled order ids and expired order ids overlap": { - blockTime: unixTimeFifteen, - expiredStatefulOrderIds: []types.OrderId{ - constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_StopLoss20.OrderId, - }, - setupState: func(ctx sdk.Context, ks keepertest.ClobKeepersTestContext, m *mocks.MemClob) { - expiredOrder := constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_StopLoss20 - - ks.ClobKeeper.SetLongTermOrderPlacement(ctx, expiredOrder, blockHeight) - ks.ClobKeeper.AddStatefulOrderIdExpiration( - ctx, - expiredOrder.MustGetUnixGoodTilBlockTime(), - expiredOrder.OrderId, - ) - - for _, orderId := range []types.OrderId{ - expiredOrder.OrderId, - } { - ks.ClobKeeper.AddDeliveredCancelledOrderId( - ctx, - orderId, - ) - } - - ks.ClobKeeper.MustSetProcessProposerMatchesEvents( - ctx, - types.ProcessProposerMatchesEvents{ - BlockHeight: blockHeight, - }, - ) - }, - expectedPanicMessage: fmt.Sprintf( - "PruneUntriggeredConditionalOrders: duplicate order id %+v in expired and "+ - "cancelled order lists", constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_StopLoss20.OrderId, - ), - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - memClob := &mocks.MemClob{} - memClob.On("SetClobKeeper", mock.Anything).Return() - - mockIndexerEventManager := &mocks.IndexerEventManager{} - - ks := keepertest.NewClobKeepersTestContext(t, memClob, &mocks.BankKeeper{}, mockIndexerEventManager) - ctx := ks.Ctx.WithBlockHeight(int64(blockHeight)).WithBlockTime(tc.blockTime) - - for _, orderId := range tc.expiredStatefulOrderIds { - mockIndexerEventManager.On("AddBlockEvent", - ctx, - indexerevents.SubtypeStatefulOrder, - indexer_manager.IndexerTendermintEvent_BLOCK_EVENT_END_BLOCK, - indexerevents.StatefulOrderEventVersion, - indexer_manager.GetBytes( - indexerevents.NewStatefulOrderRemovalEvent( - orderId, - indexershared.OrderRemovalReason_ORDER_REMOVAL_REASON_EXPIRED, - ), - ), - ).Once().Return() - } - - tc.setupState(ctx, ks, memClob) - - require.PanicsWithValue( - t, - tc.expectedPanicMessage, - func() { - clob.EndBlocker( - ctx, - *ks.ClobKeeper, - ) - }, - ) - }) - } -} - func TestEndBlocker_Success(t *testing.T) { prunedOrderIdOne := types.OrderId{SubaccountId: constants.Alice_Num0, ClientId: 0} prunedOrderIdTwo := types.OrderId{SubaccountId: constants.Alice_Num0, ClientId: 1} @@ -228,126 +140,6 @@ func TestEndBlocker_Success(t *testing.T) { OrderIdsFilledInLastBlock: []types.OrderId{prunedOrderIdTwo, orderIdThree}, }, }, - "Prunes expired and cancelled untriggered conditional orders from UntriggeredConditionalorders": { - blockTime: unixTimeFifteen, - setupState: func(ctx sdk.Context, ks keepertest.ClobKeepersTestContext, m *mocks.MemClob) { - // expired orders - orderToPrune1 := constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_StopLoss20 - orderToPrune2 := constants.ConditionalOrder_Alice_Num0_Id1_Clob0_Buy15_Price25_GTBT15_StopLoss25 - orderToPrune3 := constants.ConditionalOrder_Alice_Num0_Id0_Clob1_Buy5_Price10_GTBT15_TakeProfit20 - // cancelled order - orderToPrune4 := constants.ConditionalOrder_Alice_Num1_Id1_Clob0_Sell50_Price5_GTB30_TakeProfit20 - - // add expired orders to state, cancelled orders already removed in DeliverTx - orders := []types.Order{ - orderToPrune1, - orderToPrune2, - orderToPrune3, - } - for _, order := range orders { - ks.ClobKeeper.SetLongTermOrderPlacement(ctx, order, 0) - ks.ClobKeeper.AddStatefulOrderIdExpiration( - ctx, - order.MustGetUnixGoodTilBlockTime(), - order.OrderId, - ) - } - - ks.ClobKeeper.UntriggeredConditionalOrders = map[types.ClobPairId]*keeper.UntriggeredConditionalOrders{ - constants.ClobPair_Btc.GetClobPairId(): { - OrdersToTriggerWhenOraclePriceLTETriggerPrice: []types.Order{orderToPrune1, orderToPrune2, orderToPrune4}, - OrdersToTriggerWhenOraclePriceGTETriggerPrice: []types.Order{}, - }, - constants.ClobPair_Eth.GetClobPairId(): { - OrdersToTriggerWhenOraclePriceLTETriggerPrice: []types.Order{}, - OrdersToTriggerWhenOraclePriceGTETriggerPrice: []types.Order{orderToPrune3}, - }, - } - - for _, orderId := range []types.OrderId{ - orderToPrune4.OrderId, - } { - ks.ClobKeeper.AddDeliveredCancelledOrderId( - ctx, - orderId, - ) - } - - ks.ClobKeeper.MustSetProcessProposerMatchesEvents( - ctx, - types.ProcessProposerMatchesEvents{ - BlockHeight: blockHeight, - }, - ) - }, - expectedUntriggeredConditionalOrders: map[types.ClobPairId]*keeper.UntriggeredConditionalOrders{}, - expectedStatefulPlacementInState: map[types.OrderId]bool{ - constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_StopLoss20.OrderId: false, - constants.ConditionalOrder_Alice_Num0_Id1_Clob0_Buy15_Price25_GTBT15_StopLoss25.OrderId: false, - constants.ConditionalOrder_Alice_Num0_Id0_Clob1_Buy5_Price10_GTBT15_TakeProfit20.OrderId: false, - constants.ConditionalOrder_Alice_Num1_Id1_Clob0_Sell50_Price5_GTB30_TakeProfit20.OrderId: false, - }, - expectedProcessProposerMatchesEvents: types.ProcessProposerMatchesEvents{ - ExpiredStatefulOrderIds: []types.OrderId{ - constants.ConditionalOrder_Alice_Num0_Id1_Clob0_Buy15_Price25_GTBT15_StopLoss25.OrderId, - constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_StopLoss20.OrderId, - constants.ConditionalOrder_Alice_Num0_Id0_Clob1_Buy5_Price10_GTBT15_TakeProfit20.OrderId, - }, - BlockHeight: blockHeight, - }, - }, - `Adds newly-placed conditional order to UntriggeredConditionalOrders, but does not add - cancelled order`: { - blockTime: unixTimeTen, - setupState: func(ctx sdk.Context, ks keepertest.ClobKeepersTestContext, m *mocks.MemClob) { - orders := []types.Order{ - constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_TakeProfit10, - constants.ConditionalOrder_Alice_Num0_Id1_Clob0_Buy15_Price10_GTBT15_TakeProfit5, - constants.ConditionalOrder_Alice_Num0_Id2_Clob0_Buy20_Price10_GTBT15_TakeProfit10, - } - for _, order := range orders { - ks.ClobKeeper.SetLongTermOrderPlacement(ctx, order, blockHeight) - } - - for _, orderId := range []types.OrderId{ - constants.ConditionalOrder_Alice_Num0_Id2_Clob0_Buy20_Price10_GTBT15_TakeProfit10.OrderId, - } { - ks.ClobKeeper.AddDeliveredCancelledOrderId( - ctx, - orderId, - ) - } - - for _, orderId := range []types.OrderId{ - constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_TakeProfit10.OrderId, - constants.ConditionalOrder_Alice_Num0_Id2_Clob0_Buy20_Price10_GTBT15_TakeProfit10.OrderId, - } { - ks.ClobKeeper.AddDeliveredConditionalOrderId( - ctx, - orderId, - ) - } - - ks.ClobKeeper.MustSetProcessProposerMatchesEvents( - ctx, - types.ProcessProposerMatchesEvents{ - - BlockHeight: blockHeight, - }, - ) - }, - expectedUntriggeredConditionalOrders: map[types.ClobPairId]*keeper.UntriggeredConditionalOrders{ - constants.ClobPair_Btc.GetClobPairId(): { - OrdersToTriggerWhenOraclePriceLTETriggerPrice: []types.Order{ - constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_TakeProfit10, - }, - OrdersToTriggerWhenOraclePriceGTETriggerPrice: []types.Order{}, - }, - }, - expectedProcessProposerMatchesEvents: types.ProcessProposerMatchesEvents{ - BlockHeight: blockHeight, - }, - }, `Polls triggered conditional orders from UntriggeredConditionalOrders, update state and ProcessProposerMatchesEvents`: { blockTime: unixTimeTen, @@ -381,42 +173,25 @@ func TestEndBlocker_Success(t *testing.T) { }) require.NoError(t, err) - ks.ClobKeeper.UntriggeredConditionalOrders = map[types.ClobPairId]*keeper.UntriggeredConditionalOrders{ - constants.ClobPair_Btc.GetClobPairId(): { - // 10 oracle price subticks triggers 3 orders here. - OrdersToTriggerWhenOraclePriceLTETriggerPrice: []types.Order{ - constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_TakeProfit10, - constants.ConditionalOrder_Alice_Num0_Id1_Clob0_Buy15_Price10_GTBT15_TakeProfit5, - constants.ConditionalOrder_Alice_Num0_Id2_Clob0_Buy20_Price10_GTBT15_TakeProfit10, - constants.ConditionalOrder_Alice_Num0_Id3_Clob0_Sell25_Price10_GTBT15_StopLoss10, - }, - // 10 oracle price subticks triggers no orders here. - OrdersToTriggerWhenOraclePriceGTETriggerPrice: []types.Order{ - constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price20_GTBT15_StopLoss20, - constants.ConditionalOrder_Alice_Num0_Id1_Clob0_Buy15_Price25_GTBT15_StopLoss25, - constants.ConditionalOrder_Alice_Num0_Id2_Clob0_Sell20_Price20_GTBT15_TakeProfit20, - constants.ConditionalOrder_Alice_Num0_Id3_Clob0_Buy25_Price25_GTBT15_StopLoss25, - }, - }, - constants.ClobPair_Eth.GetClobPairId(): { - // 35 oracle price subticks triggers no orders here. - OrdersToTriggerWhenOraclePriceLTETriggerPrice: []types.Order{ - constants.ConditionalOrder_Alice_Num0_Id0_Clob1_Buy5_Price10_GTBT15_TakeProfit30, - }, - // 35 oracle price subticks triggers one order here. - OrdersToTriggerWhenOraclePriceGTETriggerPrice: []types.Order{ - constants.ConditionalOrder_Alice_Num0_Id3_Clob1_Buy25_Price10_GTBT15_StopLoss20, - }, - }, + untrigCondOrders := []types.Order{ + // 10 oracle price subticks triggers 3 orders here. + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_TakeProfit10, + constants.ConditionalOrder_Alice_Num0_Id1_Clob0_Buy15_Price10_GTBT15_TakeProfit5, + constants.ConditionalOrder_Alice_Num0_Id2_Clob0_Buy20_Price10_GTBT15_TakeProfit10, + constants.ConditionalOrder_Alice_Num0_Id3_Clob0_Sell25_Price10_GTBT15_StopLoss10, + // 10 oracle price subticks triggers no orders here. + constants.ConditionalOrder_Alice_Num1_Id0_Clob0_Buy5_Price20_GTBT15_StopLoss20, + constants.ConditionalOrder_Alice_Num1_Id1_Clob0_Buy15_Price25_GTBT15_StopLoss25, + constants.ConditionalOrder_Alice_Num1_Id2_Clob0_Sell20_Price20_GTBT15_TakeProfit20, + constants.ConditionalOrder_Alice_Num1_Id3_Clob0_Buy25_Price25_GTBT15_StopLoss25, + // 35 oracle price subticks triggers no orders here. + constants.ConditionalOrder_Alice_Num0_Id0_Clob1_Buy5_Price10_GTBT15_TakeProfit30, + // 35 oracle price subticks triggers one order here. + constants.ConditionalOrder_Alice_Num0_Id3_Clob1_Buy25_Price10_GTBT15_StopLoss20, } - for _, untrigCondOrders := range ks.ClobKeeper.UntriggeredConditionalOrders { - for _, conditionalOrder := range untrigCondOrders.OrdersToTriggerWhenOraclePriceGTETriggerPrice { - ks.ClobKeeper.SetLongTermOrderPlacement(ctx, conditionalOrder, blockHeight) - } - for _, conditionalOrder := range untrigCondOrders.OrdersToTriggerWhenOraclePriceLTETriggerPrice { - ks.ClobKeeper.SetLongTermOrderPlacement(ctx, conditionalOrder, blockHeight) - } + for _, order := range untrigCondOrders { + ks.ClobKeeper.SetLongTermOrderPlacement(ctx, order, blockHeight) } ks.ClobKeeper.MustSetProcessProposerMatchesEvents( @@ -448,10 +223,10 @@ func TestEndBlocker_Success(t *testing.T) { constants.ConditionalOrder_Alice_Num0_Id1_Clob0_Buy15_Price10_GTBT15_TakeProfit5, }, OrdersToTriggerWhenOraclePriceGTETriggerPrice: []types.Order{ - constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price20_GTBT15_StopLoss20, - constants.ConditionalOrder_Alice_Num0_Id1_Clob0_Buy15_Price25_GTBT15_StopLoss25, - constants.ConditionalOrder_Alice_Num0_Id2_Clob0_Sell20_Price20_GTBT15_TakeProfit20, - constants.ConditionalOrder_Alice_Num0_Id3_Clob0_Buy25_Price25_GTBT15_StopLoss25, + constants.ConditionalOrder_Alice_Num1_Id0_Clob0_Buy5_Price20_GTBT15_StopLoss20, + constants.ConditionalOrder_Alice_Num1_Id1_Clob0_Buy15_Price25_GTBT15_StopLoss25, + constants.ConditionalOrder_Alice_Num1_Id2_Clob0_Sell20_Price20_GTBT15_TakeProfit20, + constants.ConditionalOrder_Alice_Num1_Id3_Clob0_Buy25_Price25_GTBT15_StopLoss25, }, }, constants.ClobPair_Eth.GetClobPairId(): { @@ -835,10 +610,16 @@ func TestEndBlocker_Success(t *testing.T) { } if tc.expectedUntriggeredConditionalOrders != nil { + // Get untriggered orders from state and convert into + // `map[types.ClobPairId]*keeper.UntriggeredConditionalOrders`. + gotUntriggered := keeper.OrganizeUntriggeredConditionalOrdersFromState( + ks.ClobKeeper.GetAllUntriggeredConditionalOrders(ctx), + ) + require.Equal( t, tc.expectedUntriggeredConditionalOrders, - ks.ClobKeeper.UntriggeredConditionalOrders, + gotUntriggered, ) } diff --git a/protocol/x/clob/keeper/clob_pair_test.go b/protocol/x/clob/keeper/clob_pair_test.go index 34ff9ecd2d..dcf9a27e52 100644 --- a/protocol/x/clob/keeper/clob_pair_test.go +++ b/protocol/x/clob/keeper/clob_pair_test.go @@ -21,7 +21,6 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/testutil/nullify" perptest "github.com/dydxprotocol/v4-chain/protocol/testutil/perpetuals" pricestest "github.com/dydxprotocol/v4-chain/protocol/testutil/prices" - "github.com/dydxprotocol/v4-chain/protocol/x/clob/keeper" "github.com/dydxprotocol/v4-chain/protocol/x/clob/memclob" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals" @@ -684,10 +683,6 @@ func TestUpdateClobPair_FinalSettlement(t *testing.T) { } } - ks.ClobKeeper.UntriggeredConditionalOrders = map[types.ClobPairId]*keeper.UntriggeredConditionalOrders{ - 0: {}, // leaving blank, orders here don't matter in particular since we clear the whole key - } - clobPair.Status = types.ClobPair_STATUS_FINAL_SETTLEMENT err = ks.ClobKeeper.UpdateClobPair(ks.Ctx, clobPair) require.NoError(t, err) @@ -711,10 +706,6 @@ func TestUpdateClobPair_FinalSettlement(t *testing.T) { }, ppme.RemovedStatefulOrderIds, ) - - // Verify UntriggeredConditionalOrders is cleared. - _, found := ks.ClobKeeper.UntriggeredConditionalOrders[0] - require.False(t, found) } func TestUpdateClobPair(t *testing.T) { diff --git a/protocol/x/clob/keeper/final_settlement.go b/protocol/x/clob/keeper/final_settlement.go index a553985cac..48f4c23e29 100644 --- a/protocol/x/clob/keeper/final_settlement.go +++ b/protocol/x/clob/keeper/final_settlement.go @@ -14,9 +14,6 @@ import ( func (k Keeper) mustTransitionToFinalSettlement(ctx sdk.Context, clobPairId types.ClobPairId) { // Forcefully cancel all stateful orders from state for this clob pair. k.mustCancelStatefulOrdersForFinalSettlement(ctx, clobPairId) - - // Delete untriggered conditional orders for this clob pair from memory. - delete(k.UntriggeredConditionalOrders, clobPairId) } // mustCancelStatefulOrdersForFinalSettlement forcefully cancels all stateful orders diff --git a/protocol/x/clob/keeper/keeper.go b/protocol/x/clob/keeper/keeper.go index 4ea1edee3a..5ab67186e1 100644 --- a/protocol/x/clob/keeper/keeper.go +++ b/protocol/x/clob/keeper/keeper.go @@ -28,9 +28,8 @@ type ( transientStoreKey storetypes.StoreKey authorities map[string]struct{} - MemClob types.MemClob - UntriggeredConditionalOrders map[types.ClobPairId]*UntriggeredConditionalOrders - PerpetualIdToClobPairId map[uint32][]types.ClobPairId + MemClob types.MemClob + PerpetualIdToClobPairId map[uint32][]types.ClobPairId subaccountsKeeper types.SubaccountsKeeper assetsKeeper types.AssetsKeeper @@ -92,28 +91,27 @@ func NewKeeper( daemonLiquidationInfo *liquidationtypes.DaemonLiquidationInfo, ) *Keeper { keeper := &Keeper{ - cdc: cdc, - storeKey: storeKey, - memKey: memKey, - transientStoreKey: liquidationsStoreKey, - authorities: lib.UniqueSliceToSet(authorities), - MemClob: memClob, - UntriggeredConditionalOrders: make(map[types.ClobPairId]*UntriggeredConditionalOrders), - PerpetualIdToClobPairId: make(map[uint32][]types.ClobPairId), - subaccountsKeeper: subaccountsKeeper, - assetsKeeper: assetsKeeper, - blockTimeKeeper: blockTimeKeeper, - bankKeeper: bankKeeper, - feeTiersKeeper: feeTiersKeeper, - perpetualsKeeper: perpetualsKeeper, - pricesKeeper: pricesKeeper, - statsKeeper: statsKeeper, - rewardsKeeper: rewardsKeeper, - indexerEventManager: indexerEventManager, - streamingManager: grpcStreamingManager, - memStoreInitialized: &atomic.Bool{}, // False by default. - initialized: &atomic.Bool{}, // False by default. - txDecoder: txDecoder, + cdc: cdc, + storeKey: storeKey, + memKey: memKey, + transientStoreKey: liquidationsStoreKey, + authorities: lib.UniqueSliceToSet(authorities), + MemClob: memClob, + PerpetualIdToClobPairId: make(map[uint32][]types.ClobPairId), + subaccountsKeeper: subaccountsKeeper, + assetsKeeper: assetsKeeper, + blockTimeKeeper: blockTimeKeeper, + bankKeeper: bankKeeper, + feeTiersKeeper: feeTiersKeeper, + perpetualsKeeper: perpetualsKeeper, + pricesKeeper: pricesKeeper, + statsKeeper: statsKeeper, + rewardsKeeper: rewardsKeeper, + indexerEventManager: indexerEventManager, + streamingManager: grpcStreamingManager, + memStoreInitialized: &atomic.Bool{}, // False by default. + initialized: &atomic.Bool{}, // False by default. + txDecoder: txDecoder, mevTelemetryConfig: MevTelemetryConfig{ Enabled: clobFlags.MevTelemetryEnabled, Hosts: clobFlags.MevTelemetryHosts, @@ -188,9 +186,6 @@ func (k Keeper) Initialize(ctx sdk.Context) { // Initialize the untriggered conditional orders data structure with untriggered // conditional orders in state. k.HydrateClobPairAndPerpetualMapping(checkCtx) - // Initialize the untriggered conditional orders data structure with untriggered - // conditional orders in state. - k.HydrateUntriggeredConditionalOrders(checkCtx) } // InitMemStore initializes the memstore of the `clob` keeper. diff --git a/protocol/x/clob/keeper/orders.go b/protocol/x/clob/keeper/orders.go index 4d6ec5f0d3..93c2e795de 100644 --- a/protocol/x/clob/keeper/orders.go +++ b/protocol/x/clob/keeper/orders.go @@ -1243,38 +1243,6 @@ func (k Keeper) InitStatefulOrders( } } -// HydrateUntriggeredConditionalOrders inserts all untriggered conditional orders in state into the -// `UntriggeredConditionalOrders` data structure. Note that all untriggered conditional orders will -// be ordered by time priority. This function should only be called on application startup. -func (k Keeper) HydrateUntriggeredConditionalOrders( - ctx sdk.Context, -) { - defer telemetry.ModuleMeasureSince( - types.ModuleName, - time.Now(), - metrics.ConditionalOrderUntriggered, - metrics.Hydrate, - metrics.Latency, - ) - - // Get all untriggered conditional orders in state, ordered by time priority ascending order, - // and add them to the `UntriggeredConditionalOrders` data structure. - untriggeredConditionalOrders := k.GetAllUntriggeredConditionalOrders(ctx) - k.AddUntriggeredConditionalOrders( - ctx, - lib.MapSlice( - untriggeredConditionalOrders, - func(o types.Order) types.OrderId { - return o.OrderId - }, - ), - // Note both of these arguments are empty slices since the untriggered conditional orders - // shouldn't be expired or canceled. - map[types.OrderId]struct{}{}, - map[types.OrderId]struct{}{}, - ) -} - // sendOffchainMessagesWithTxHash sends all the `Message` in the offchainUpdates passed in along with // an additional header for the transaction hash passed in. func (k Keeper) sendOffchainMessagesWithTxHash( diff --git a/protocol/x/clob/keeper/orders_test.go b/protocol/x/clob/keeper/orders_test.go index 14b5670833..b40d2c2fcd 100644 --- a/protocol/x/clob/keeper/orders_test.go +++ b/protocol/x/clob/keeper/orders_test.go @@ -2048,125 +2048,6 @@ func TestInitStatefulOrders(t *testing.T) { } } -func TestHydrateUntriggeredConditionalOrdersInMemClob(t *testing.T) { - tests := map[string]struct { - // CLOB module state. - statefulOrdersInState []types.Order - isConditionalOrderTriggered map[types.OrderId]bool - }{ - `Can initialize untriggered conditional orders with 0 stateful orders in state`: { - statefulOrdersInState: []types.Order{}, - isConditionalOrderTriggered: map[types.OrderId]bool{}, - }, - `Can initialize untriggered conditional orders with both Long-Term and triggered - conditional orders in state`: { - statefulOrdersInState: []types.Order{ - constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50005, - constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy100_Price10_GTBT15_PO, - }, - isConditionalOrderTriggered: map[types.OrderId]bool{ - constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50005. - OrderId: true, - }, - }, - `Can initialize untriggered conditional orders with both Long-Term, untriggered conditional - orders, and triggered conditional orders in state`: { - statefulOrdersInState: []types.Order{ - constants.ConditionalOrder_Carl_Num0_Id0_Clob0_Buy1BTC_Price50000_GTBT10, - constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50005, - constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy100_Price10_GTBT15_PO, - constants.ConditionalOrder_Alice_Num0_Id1_Clob0_Buy15_Price10_GTBT15_TakeProfit20, - constants.LongTermOrder_Bob_Num0_Id0_Clob0_Buy25_Price30_GTBT10, - }, - isConditionalOrderTriggered: map[types.OrderId]bool{ - constants.ConditionalOrder_Bob_Num0_Id0_Clob0_Sell1BTC_Price50000_GTBT10_TP_50005. - OrderId: true, - }, - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - // Setup state. - memClob := &mocks.MemClob{} - memClob.On("SetClobKeeper", mock.Anything).Return() - - indexerEventManager := &mocks.IndexerEventManager{} - - ks := keepertest.NewClobKeepersTestContext(t, memClob, &mocks.BankKeeper{}, indexerEventManager) - prices.InitGenesis(ks.Ctx, *ks.PricesKeeper, constants.Prices_DefaultGenesisState) - perpetuals.InitGenesis(ks.Ctx, *ks.PerpetualsKeeper, constants.Perpetuals_DefaultGenesisState) - - // Create CLOB pair. - memClob.On("CreateOrderbook", mock.Anything, constants.ClobPair_Btc).Return() - indexerEventManager.On("AddTxnEvent", - ks.Ctx, - indexerevents.SubtypePerpetualMarket, - indexerevents.PerpetualMarketEventVersion, - indexer_manager.GetBytes( - indexerevents.NewPerpetualMarketCreateEvent( - 0, - 0, - constants.Perpetuals_DefaultGenesisState.Perpetuals[0].Params.Ticker, - constants.Perpetuals_DefaultGenesisState.Perpetuals[0].Params.MarketId, - constants.ClobPair_Btc.Status, - constants.ClobPair_Btc.QuantumConversionExponent, - constants.Perpetuals_DefaultGenesisState.Perpetuals[0].Params.AtomicResolution, - constants.ClobPair_Btc.SubticksPerTick, - constants.ClobPair_Btc.StepBaseQuantums, - constants.Perpetuals_DefaultGenesisState.Perpetuals[0].Params.LiquidityTier, - constants.Perpetuals_DefaultGenesisState.Perpetuals[0].Params.MarketType, - ), - ), - ).Once().Return() - _, err := ks.ClobKeeper.CreatePerpetualClobPair( - ks.Ctx, - constants.ClobPair_Btc.Id, - clobtest.MustPerpetualId(constants.ClobPair_Btc), - satypes.BaseQuantums(constants.ClobPair_Btc.StepBaseQuantums), - constants.ClobPair_Btc.QuantumConversionExponent, - constants.ClobPair_Btc.SubticksPerTick, - constants.ClobPair_Btc.Status, - ) - require.NoError(t, err) - - // Create each stateful order placement in state. - expectedUntriggeredConditionalOrders := make(map[types.ClobPairId]*keeper.UntriggeredConditionalOrders) - for i, order := range tc.statefulOrdersInState { - require.True(t, order.IsStatefulOrder()) - - // Write the stateful order placement to state. - ks.ClobKeeper.SetLongTermOrderPlacement(ks.Ctx, order, uint32(i)) - - // No further state updates are required if this isn't a conditional order. - if !order.IsConditionalOrder() { - continue - } - - // If it's a triggered conditional order, ensure it's triggered in state and skip - // updating the expected untriggered conditional orders. - if tc.isConditionalOrderTriggered[order.OrderId] { - ks.ClobKeeper.MustTriggerConditionalOrder(ks.Ctx, order.OrderId) - continue - } - - // This is an untriggered conditional order and we expect it to be returned. - untriggeredConditionalOrders, exists := expectedUntriggeredConditionalOrders[order.GetClobPairId()] - if !exists { - untriggeredConditionalOrders = ks.ClobKeeper.NewUntriggeredConditionalOrders() - expectedUntriggeredConditionalOrders[order.GetClobPairId()] = untriggeredConditionalOrders - } - untriggeredConditionalOrders.AddUntriggeredConditionalOrder(order) - } - - // Run the test and verify expectations. - ks.ClobKeeper.HydrateUntriggeredConditionalOrders(ks.Ctx) - - require.Equal(t, expectedUntriggeredConditionalOrders, ks.ClobKeeper.UntriggeredConditionalOrders) - }) - } -} - func TestPlaceStatefulOrdersFromLastBlock(t *testing.T) { tests := map[string]struct { orders []types.Order diff --git a/protocol/x/clob/keeper/process_operations.go b/protocol/x/clob/keeper/process_operations.go index 2f5a38e2fd..566e25a41d 100644 --- a/protocol/x/clob/keeper/process_operations.go +++ b/protocol/x/clob/keeper/process_operations.go @@ -506,7 +506,7 @@ func (k Keeper) PersistMatchOrdersToState( } makerOrders = append(makerOrders, makerOrder) - _, _, _, _, err = k.ProcessSingleMatch(ctx, &matchWithOrders) + _, _, _, err = k.ProcessSingleMatch(ctx, &matchWithOrders) if err != nil { return err } @@ -614,7 +614,7 @@ func (k Keeper) PersistMatchLiquidationToState( // Write the position updates and state fill amounts for this match. // Note stateless validation on the constructed `matchWithOrders` is performed within this function. - _, _, _, _, err = k.ProcessSingleMatch( + _, _, _, err = k.ProcessSingleMatch( ctx, &matchWithOrders, ) diff --git a/protocol/x/clob/keeper/process_operations_test.go b/protocol/x/clob/keeper/process_operations_test.go index 0e1579b4be..5b8a6cb32d 100644 --- a/protocol/x/clob/keeper/process_operations_test.go +++ b/protocol/x/clob/keeper/process_operations_test.go @@ -2331,7 +2331,6 @@ func setupProcessProposerOperationsTestCase( tc.rawOperations, ) } else { - mockIndexerEventManager.On("Enabled").Return(false).Maybe() mockIndexerEventManager.On("AddTxnEvent", mock.Anything, mock.Anything, @@ -2566,10 +2565,6 @@ func setupNewMockEventManager( matches []*MatchWithOrdersForTesting, rawOperations []types.OperationRaw, ) { - if len(matches) > 0 { - mockIndexerEventManager.On("Enabled").Return(true) - } - // Add an expectation to the mock for each expected message. var matchOrderCallMap = make(map[types.OrderId]*mock.Call) for _, match := range matches { diff --git a/protocol/x/clob/keeper/process_single_match.go b/protocol/x/clob/keeper/process_single_match.go index 67b0cfddad..fd132552a3 100644 --- a/protocol/x/clob/keeper/process_single_match.go +++ b/protocol/x/clob/keeper/process_single_match.go @@ -9,7 +9,6 @@ import ( errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/dydxprotocol/v4-chain/protocol/indexer/off_chain_updates" "github.com/dydxprotocol/v4-chain/protocol/lib" "github.com/dydxprotocol/v4-chain/protocol/lib/log" "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" @@ -46,7 +45,6 @@ func (k Keeper) ProcessSingleMatch( success bool, takerUpdateResult satypes.UpdateResult, makerUpdateResult satypes.UpdateResult, - offchainUpdates *types.OffchainUpdates, err error, ) { if matchWithOrders.TakerOrder.IsLiquidation() { @@ -74,14 +72,13 @@ func (k Keeper) ProcessSingleMatch( // Perform stateless validation on the match. if err := matchWithOrders.Validate(); err != nil { - return false, takerUpdateResult, makerUpdateResult, nil, errorsmod.Wrapf( + return false, takerUpdateResult, makerUpdateResult, errorsmod.Wrapf( err, "ProcessSingleMatch: Invalid MatchWithOrders: %+v", matchWithOrders, ) } - offchainUpdates = types.NewOffchainUpdates() makerMatchableOrder := matchWithOrders.MakerOrder takerMatchableOrder := matchWithOrders.TakerOrder fillAmount := matchWithOrders.FillAmount @@ -90,7 +87,7 @@ func (k Keeper) ProcessSingleMatch( clobPairId := makerMatchableOrder.GetClobPairId() clobPair, found := k.GetClobPair(ctx, clobPairId) if !found { - return false, takerUpdateResult, makerUpdateResult, nil, types.ErrInvalidClob + return false, takerUpdateResult, makerUpdateResult, types.ErrInvalidClob } // Verify that the `fillAmount` is divisible by the `StepBaseQuantums` of the `clobPair`. @@ -98,7 +95,6 @@ func (k Keeper) ProcessSingleMatch( return false, takerUpdateResult, makerUpdateResult, - nil, types.ErrFillAmountNotDivisibleByStepSize } @@ -108,7 +104,7 @@ func (k Keeper) ProcessSingleMatch( // Calculate the number of quote quantums for the match based on the maker order subticks. bigFillQuoteQuantums, err := getFillQuoteQuantums(clobPair, makerSubticks, fillAmount) if err != nil { - return false, takerUpdateResult, makerUpdateResult, nil, err + return false, takerUpdateResult, makerUpdateResult, err } if bigFillQuoteQuantums.Sign() == 0 { @@ -131,7 +127,7 @@ func (k Keeper) ProcessSingleMatch( // Retrieve the associated perpetual id for the `ClobPair`. perpetualId, err := clobPair.GetPerpetualId() if err != nil { - return false, takerUpdateResult, makerUpdateResult, nil, err + return false, takerUpdateResult, makerUpdateResult, err } // Calculate taker and maker fee ppms. @@ -157,7 +153,7 @@ func (k Keeper) ProcessSingleMatch( ) if err != nil { - return false, takerUpdateResult, makerUpdateResult, nil, err + return false, takerUpdateResult, makerUpdateResult, err } } @@ -190,7 +186,7 @@ func (k Keeper) ProcessSingleMatch( ) if err != nil { - return false, takerUpdateResult, makerUpdateResult, nil, err + return false, takerUpdateResult, makerUpdateResult, err } } @@ -212,7 +208,7 @@ func (k Keeper) ProcessSingleMatch( ) if err != nil { - return false, takerUpdateResult, makerUpdateResult, nil, err + return false, takerUpdateResult, makerUpdateResult, err } // Update both subaccounts in the matched order atomically. @@ -227,7 +223,7 @@ func (k Keeper) ProcessSingleMatch( ) if err != nil { - return false, takerUpdateResult, makerUpdateResult, nil, err + return false, takerUpdateResult, makerUpdateResult, err } // Update subaccount total quantums liquidated and total insurance fund lost for liquidation orders. @@ -238,7 +234,7 @@ func (k Keeper) ProcessSingleMatch( fillAmount.ToBigInt(), ) if err != nil { - return false, takerUpdateResult, makerUpdateResult, nil, err + return false, takerUpdateResult, makerUpdateResult, err } k.UpdateSubaccountLiquidationInfo( @@ -275,24 +271,22 @@ func (k Keeper) ProcessSingleMatch( // Liquidation orders can only be placed when a subaccount is liquidatable // and cannot be replayed, therefore we don't need to track their filled amount in state. if !matchWithOrders.TakerOrder.IsLiquidation() { - takerOffchainUpdates := k.setOrderFillAmountsAndPruning( + k.setOrderFillAmountsAndPruning( ctx, matchWithOrders.TakerOrder.MustGetOrder(), newTakerTotalFillAmount, curTakerPruneableBlockHeight, ) - offchainUpdates.Append(takerOffchainUpdates) } - makerOffchainUpdates := k.setOrderFillAmountsAndPruning( + k.setOrderFillAmountsAndPruning( ctx, matchWithOrders.MakerOrder.MustGetOrder(), newMakerTotalFillAmount, curMakerPruneableBlockHeight, ) - offchainUpdates.Append(makerOffchainUpdates) - return true, takerUpdateResult, makerUpdateResult, offchainUpdates, nil + return true, takerUpdateResult, makerUpdateResult, nil } // persistMatchedOrders persists a matched order to the subaccount state, @@ -501,10 +495,9 @@ func (k Keeper) setOrderFillAmountsAndPruning( order types.Order, newTotalFillAmount satypes.BaseQuantums, curPruneableBlockHeight uint32, -) *types.OffchainUpdates { +) { // Note that stateful orders are never pruned by `BlockHeight`, so we set the value to `math.MaxUint32` here. pruneableBlockHeight := uint32(math.MaxUint32) - offchainUpdates := types.NewOffchainUpdates() if !order.IsStatefulOrder() { // Compute the block at which this state fill amount can be pruned. This is the greater of @@ -540,21 +533,6 @@ func (k Keeper) setOrderFillAmountsAndPruning( newTotalFillAmount, pruneableBlockHeight, ) - - if k.GetIndexerEventManager().Enabled() { - if _, exists := k.MemClob.GetOrder(ctx, order.OrderId); exists { - // Generate an off-chain update message updating the total filled amount of order. - if message, success := off_chain_updates.CreateOrderUpdateMessage( - ctx, - order.OrderId, - newTotalFillAmount, - ); success { - offchainUpdates.AddUpdateMessage(order.OrderId, message) - } - } - } - - return offchainUpdates } // getUpdatedOrderFillAmount accepts an order's current total fill amount, total base quantums, and a new fill amount, diff --git a/protocol/x/clob/keeper/stores.go b/protocol/x/clob/keeper/stores.go index c84350e975..623acd7965 100644 --- a/protocol/x/clob/keeper/stores.go +++ b/protocol/x/clob/keeper/stores.go @@ -78,15 +78,6 @@ func (k Keeper) GetTriggeredConditionalOrderPlacementStore(ctx sdk.Context) pref ) } -// GetTriggeredConditionalOrderPlacementMemStore fetches a state store used for creating, -// reading, updating, and deleting a stateful order placement from state. -func (k Keeper) GetTriggeredConditionalOrderPlacementMemStore(ctx sdk.Context) prefix.Store { - return prefix.NewStore( - ctx.KVStore(k.memKey), - []byte(types.TriggeredConditionalOrderKeyPrefix), - ) -} - // getTransientStore fetches a transient store used for reading and // updating the transient store. func (k Keeper) getTransientStore(ctx sdk.Context) storetypes.KVStore { diff --git a/protocol/x/clob/keeper/untriggered_conditional_orders.go b/protocol/x/clob/keeper/untriggered_conditional_orders.go index 8ff6e3a52d..ec3a0de07d 100644 --- a/protocol/x/clob/keeper/untriggered_conditional_orders.go +++ b/protocol/x/clob/keeper/untriggered_conditional_orders.go @@ -51,43 +51,6 @@ func (untriggeredOrders *UntriggeredConditionalOrders) IsEmpty() bool { len(untriggeredOrders.OrdersToTriggerWhenOraclePriceGTETriggerPrice) == 0 } -// AddUntriggeredConditionalOrders takes in a list of newly-placed conditional order ids and adds them -// to the in-memory UntriggeredConditionalOrders struct, filtering out orders that have been cancelled -// or expired in the last block. This function is used in EndBlocker and on application startup. -func (k Keeper) AddUntriggeredConditionalOrders( - ctx sdk.Context, - placedConditionalOrderIds []types.OrderId, - placedStatefulCancellationOrderIds map[types.OrderId]struct{}, - expiredStatefulOrderIdsSet map[types.OrderId]struct{}, -) { - for _, orderId := range placedConditionalOrderIds { - _, isCancelled := placedStatefulCancellationOrderIds[orderId] - _, isExpired := expiredStatefulOrderIdsSet[orderId] - if isCancelled || isExpired { - continue - } - - orderPlacement, exists := k.GetUntriggeredConditionalOrderPlacement(ctx, orderId) - if !exists { - panic( - fmt.Sprintf( - "AddUntriggeredConditionalOrders: order placement does not exist in state for untriggered "+ - "conditional order id, OrderId %+v.", - orderId, - ), - ) - } - - clobPairId := types.ClobPairId(orderId.GetClobPairId()) - untriggeredConditionalOrders, exists := k.UntriggeredConditionalOrders[clobPairId] - if !exists { - untriggeredConditionalOrders = k.NewUntriggeredConditionalOrders() - k.UntriggeredConditionalOrders[clobPairId] = untriggeredConditionalOrders - } - untriggeredConditionalOrders.AddUntriggeredConditionalOrder(orderPlacement.GetOrder()) - } -} - // AddUntriggeredConditionalOrder adds an untriggered conditional order to the UntriggeredConditionalOrders // data structure. It will panic if the order is not a conditional order. func (untriggeredOrders *UntriggeredConditionalOrders) AddUntriggeredConditionalOrder(order types.Order) { @@ -122,58 +85,6 @@ func (untriggeredOrders *UntriggeredConditionalOrders) AddUntriggeredConditional } } -// PruneUntriggeredConditionalOrders takes in lists of expired and cancelled stateful order ids and removes -// all respective orders from the in-memory `UntriggeredConditionalOrders` data structure. This data structure -// stores untriggered orders in a map of ClobPairId -> []Order, so we first group orders by ClobPairId and then -// call `UntriggeredConditionalOrders.RemoveExpiredUntriggeredConditionalOrders` on each ClobPairId. -func (k Keeper) PruneUntriggeredConditionalOrders( - expiredStatefulOrderIds []types.OrderId, - cancelledStatefulOrderIds []types.OrderId, -) { - // Merge lists of order ids. - orderIdsToPrune := lib.UniqueSliceToSet(expiredStatefulOrderIds) - for _, orderId := range cancelledStatefulOrderIds { - if _, exists := orderIdsToPrune[orderId]; exists { - panic( - fmt.Sprintf( - "PruneUntriggeredConditionalOrders: duplicate order id %+v in expired and "+ - "cancelled order lists", orderId, - ), - ) - } - orderIdsToPrune[orderId] = struct{}{} - } - - prunableUntriggeredConditionalOrderIdsByClobPair := make(map[types.ClobPairId][]types.OrderId) - for orderId := range orderIdsToPrune { - // If the order id is conditional, add to prunable list of untriggered order ids. - // Triggered conditional orders will be effectively ignored during removal as they are not part of - // UntriggeredConditionalOrders anymore. No need to filter out here, we can avoid memstore reads. - if orderId.IsConditionalOrder() { - clobPairId := types.ClobPairId(orderId.GetClobPairId()) - if _, exists := prunableUntriggeredConditionalOrderIdsByClobPair[clobPairId]; !exists { - prunableUntriggeredConditionalOrderIdsByClobPair[clobPairId] = []types.OrderId{} - } - - prunableUntriggeredConditionalOrderIdsByClobPair[clobPairId] = append( - prunableUntriggeredConditionalOrderIdsByClobPair[clobPairId], - orderId, - ) - } - } - - for clobPairId := range prunableUntriggeredConditionalOrderIdsByClobPair { - if untriggeredConditionalOrders, exists := k.UntriggeredConditionalOrders[clobPairId]; exists { - untriggeredConditionalOrders.RemoveUntriggeredConditionalOrders( - prunableUntriggeredConditionalOrderIdsByClobPair[clobPairId], - ) - if untriggeredConditionalOrders.IsEmpty() { - delete(k.UntriggeredConditionalOrders, clobPairId) - } - } - } -} - // RemoveUntriggeredConditionalOrders removes a list of order ids from the `UntriggeredConditionalOrders` // data structure. This function will panic if the order ids contained involve more than one ClobPairId. func (untriggeredOrders *UntriggeredConditionalOrders) RemoveUntriggeredConditionalOrders( @@ -263,6 +174,26 @@ func (untriggeredOrders *UntriggeredConditionalOrders) PollTriggeredConditionalO return triggeredOrderIds } +// OrganizeUntriggeredConditionalOrdersFromState takes in a list of conditional orders read from +// state, organize them and return in form of `UntriggeredConditionalOrders` struct. +func OrganizeUntriggeredConditionalOrdersFromState( + conditonalOrdersFromState []types.Order, +) map[types.ClobPairId]*UntriggeredConditionalOrders { + ret := make(map[types.ClobPairId]*UntriggeredConditionalOrders) + + for _, order := range conditonalOrdersFromState { + clobPairId := types.ClobPairId(order.GetClobPairId()) + untriggeredConditionalOrders, exists := ret[clobPairId] + if !exists { + untriggeredConditionalOrders = NewUntriggeredConditionalOrders() + ret[clobPairId] = untriggeredConditionalOrders + } + untriggeredConditionalOrders.AddUntriggeredConditionalOrder(order) + } + + return ret +} + // MaybeTriggerConditionalOrders queries the prices module for price updates and triggers // any conditional orders in `UntriggeredConditionalOrders` that can be triggered. For each triggered // order, it takes the stateful order placement stored in Untriggered state and moves it to Triggered state. @@ -277,15 +208,19 @@ func (k Keeper) MaybeTriggerConditionalOrders(ctx sdk.Context) (allTriggeredOrde time.Now(), ) + clobPairToUntriggeredConditionals := OrganizeUntriggeredConditionalOrdersFromState( + k.GetAllUntriggeredConditionalOrders(ctx), + ) + // Sort the keys for the untriggered conditional orders struct. We need to trigger // the conditional orders in an ordered way to have deterministic state writes. - sortedKeys := lib.GetSortedKeys[types.SortedClobPairId](k.UntriggeredConditionalOrders) + sortedKeys := lib.GetSortedKeys[types.SortedClobPairId](clobPairToUntriggeredConditionals) allTriggeredOrderIds = make([]types.OrderId, 0) // For all clob pair ids in UntriggeredConditionalOrders, fetch the updated // oracle price and poll out triggered conditional orders. for _, clobPairId := range sortedKeys { - untriggered := k.UntriggeredConditionalOrders[clobPairId] + untriggered := clobPairToUntriggeredConditionals[clobPairId] clobPair, found := k.GetClobPair(ctx, clobPairId) if !found { panic( @@ -319,9 +254,6 @@ func (k Keeper) MaybeTriggerConditionalOrders(ctx sdk.Context) (allTriggeredOrde allTriggeredOrderIds = append(allTriggeredOrderIds, triggered...) } - // Set the modified untriggeredConditionalOrders back on the keeper field. - k.UntriggeredConditionalOrders[clobPairId] = untriggered - // Gauge the number of untriggered orders. metrics.SetGaugeWithLabels( metrics.ClobNumUntriggeredOrders, diff --git a/protocol/x/clob/keeper/untriggered_conditional_orders_test.go b/protocol/x/clob/keeper/untriggered_conditional_orders_test.go index db84fe6f04..bfb85b41e6 100644 --- a/protocol/x/clob/keeper/untriggered_conditional_orders_test.go +++ b/protocol/x/clob/keeper/untriggered_conditional_orders_test.go @@ -2,7 +2,6 @@ package keeper_test import ( "fmt" - testApp "github.com/dydxprotocol/v4-chain/protocol/testutil/app" "math/big" "testing" @@ -88,10 +87,7 @@ func TestAddUntriggeredConditionalOrder(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { - tApp := testApp.NewTestAppBuilder(t).Build() - tApp.InitChain() - untriggeredConditionalOrders := tApp.App.ClobKeeper.NewUntriggeredConditionalOrders() - tApp.App.ClobKeeper.UntriggeredConditionalOrders[0] = untriggeredConditionalOrders + untriggeredConditionalOrders := keeper.NewUntriggeredConditionalOrders() for _, order := range tc.conditionalOrdersToAdd { untriggeredConditionalOrders.AddUntriggeredConditionalOrder(order) @@ -127,6 +123,98 @@ func TestAddUntriggeredConditionalOrder_NonConditionalOrder(t *testing.T) { ) } +func TestOrganizeUntriggeredConditionalOrdersFromState(t *testing.T) { + tests := map[string]struct { + // Setup. + conditionalOrdersFromState []types.Order + + // Expectations. + expectedUntriggeredConditionalOrders map[types.ClobPairId]*keeper.UntriggeredConditionalOrders + }{ + "Only GTE orders, one ClobPair": { + conditionalOrdersFromState: []types.Order{ + // GTE orders + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_StopLoss20, + constants.ConditionalOrder_Alice_Num0_Id1_Clob0_Buy15_Price10_GTBT15_StopLoss20, + constants.ConditionalOrder_Alice_Num0_Id2_Clob0_Buy20_Price10_GTBT15_StopLoss20, + }, + expectedUntriggeredConditionalOrders: map[types.ClobPairId]*keeper.UntriggeredConditionalOrders{ + 0: { + OrdersToTriggerWhenOraclePriceLTETriggerPrice: []types.Order{}, + OrdersToTriggerWhenOraclePriceGTETriggerPrice: []types.Order{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_StopLoss20, + constants.ConditionalOrder_Alice_Num0_Id1_Clob0_Buy15_Price10_GTBT15_StopLoss20, + constants.ConditionalOrder_Alice_Num0_Id2_Clob0_Buy20_Price10_GTBT15_StopLoss20, + }, + }, + }, + }, + "Both GTE and LTE orders, one ClobPair": { + conditionalOrdersFromState: []types.Order{ + // GTE + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_StopLoss20, + // LTE + constants.ConditionalOrder_Alice_Num0_Id3_Clob0_Buy25_Price10_GTBT15_TakeProfit20, + }, + expectedUntriggeredConditionalOrders: map[types.ClobPairId]*keeper.UntriggeredConditionalOrders{ + 0: { + OrdersToTriggerWhenOraclePriceLTETriggerPrice: []types.Order{ + constants.ConditionalOrder_Alice_Num0_Id3_Clob0_Buy25_Price10_GTBT15_TakeProfit20, + }, + OrdersToTriggerWhenOraclePriceGTETriggerPrice: []types.Order{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_StopLoss20, + }, + }, + }, + }, + "Multiple ClobPair + both LTE and GTE orders": { + conditionalOrdersFromState: []types.Order{ + // GTE, ClobPair 1 + constants.ConditionalOrder_Alice_Num0_Id0_Clob1_Buy5_Price10_GTBT15_TakeProfit20, + // GTE, ClobPair 0 + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_StopLoss20, + // LTE, ClobPair 0 + constants.ConditionalOrder_Alice_Num0_Id3_Clob0_Buy25_Price10_GTBT15_TakeProfit20, + // GTE, ClobPair 0 + constants.ConditionalOrder_Alice_Num0_Id2_Clob0_Sell20_Price20_GTBT15_TakeProfit20, + // LTE, ClobPair 1 + constants.ConditionalOrder_Alice_Num0_Id1_Clob1_Buy5_Price10_GTBT15_StopLoss20, + }, + expectedUntriggeredConditionalOrders: map[types.ClobPairId]*keeper.UntriggeredConditionalOrders{ + 0: { + OrdersToTriggerWhenOraclePriceLTETriggerPrice: []types.Order{ + constants.ConditionalOrder_Alice_Num0_Id3_Clob0_Buy25_Price10_GTBT15_TakeProfit20, + }, + OrdersToTriggerWhenOraclePriceGTETriggerPrice: []types.Order{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTBT15_StopLoss20, + constants.ConditionalOrder_Alice_Num0_Id2_Clob0_Sell20_Price20_GTBT15_TakeProfit20, + }, + }, + 1: { + OrdersToTriggerWhenOraclePriceLTETriggerPrice: []types.Order{ + constants.ConditionalOrder_Alice_Num0_Id0_Clob1_Buy5_Price10_GTBT15_TakeProfit20, + }, + OrdersToTriggerWhenOraclePriceGTETriggerPrice: []types.Order{ + constants.ConditionalOrder_Alice_Num0_Id1_Clob1_Buy5_Price10_GTBT15_StopLoss20, + }, + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := keeper.OrganizeUntriggeredConditionalOrdersFromState(tc.conditionalOrdersFromState) + + require.Equal( + t, + tc.expectedUntriggeredConditionalOrders, + got, + ) + }) + } +} + func TestRemoveUntriggeredConditionalOrders(t *testing.T) { tests := map[string]struct { // Setup. @@ -194,10 +282,7 @@ func TestRemoveUntriggeredConditionalOrders(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { - tApp := testApp.NewTestAppBuilder(t).Build() - tApp.InitChain() - untriggeredConditionalOrders := tApp.App.ClobKeeper.NewUntriggeredConditionalOrders() - tApp.App.ClobKeeper.UntriggeredConditionalOrders[0] = untriggeredConditionalOrders + untriggeredConditionalOrders := keeper.NewUntriggeredConditionalOrders() for _, order := range tc.conditionalOrdersToAdd { untriggeredConditionalOrders.AddUntriggeredConditionalOrder(order) diff --git a/protocol/x/clob/memclob/memclob.go b/protocol/x/clob/memclob/memclob.go index 5a4a88ce7d..138314d044 100644 --- a/protocol/x/clob/memclob/memclob.go +++ b/protocol/x/clob/memclob/memclob.go @@ -1686,7 +1686,7 @@ func (m *MemClobPriceTimePriority) mustPerformTakerOrderMatching( FillAmount: matchedAmount, } - success, takerUpdateResult, makerUpdateResult, _, err := m.clobKeeper.ProcessSingleMatch(ctx, &matchWithOrders) + success, takerUpdateResult, makerUpdateResult, err := m.clobKeeper.ProcessSingleMatch(ctx, &matchWithOrders) if err != nil && !errors.Is(err, satypes.ErrFailedToUpdateSubaccounts) { if errors.Is(err, types.ErrLiquidationExceedsSubaccountMaxInsuranceLost) { // Subaccount has reached max insurance lost block limit. Stop matching. diff --git a/protocol/x/clob/types/clob_keeper.go b/protocol/x/clob/types/clob_keeper.go index 61f1675e2c..5c3b3c3cb9 100644 --- a/protocol/x/clob/types/clob_keeper.go +++ b/protocol/x/clob/types/clob_keeper.go @@ -88,7 +88,6 @@ type ClobKeeper interface { success bool, takerUpdateResult satypes.UpdateResult, makerUpdateResult satypes.UpdateResult, - offchainUpdates *OffchainUpdates, err error, ) SetLongTermOrderPlacement( diff --git a/protocol/x/clob/types/mem_clob_keeper.go b/protocol/x/clob/types/mem_clob_keeper.go index c8670d694a..d81758303b 100644 --- a/protocol/x/clob/types/mem_clob_keeper.go +++ b/protocol/x/clob/types/mem_clob_keeper.go @@ -25,7 +25,6 @@ type MemClobKeeper interface { success bool, takerUpdateResult satypes.UpdateResult, makerUpdateResult satypes.UpdateResult, - offchainUpdates *OffchainUpdates, err error, ) CanDeleverageSubaccount( diff --git a/protocol/x/prices/keeper/keeper.go b/protocol/x/prices/keeper/keeper.go index 47533ffcc4..56ecc7aa18 100644 --- a/protocol/x/prices/keeper/keeper.go +++ b/protocol/x/prices/keeper/keeper.go @@ -3,7 +3,6 @@ package keeper import ( "fmt" "sync/atomic" - "time" "cosmossdk.io/log" storetypes "cosmossdk.io/store/types" @@ -23,7 +22,6 @@ type ( indexPriceCache *pricefeedtypes.MarketToExchangePrices timeProvider libtime.TimeProvider indexerEventManager indexer_manager.IndexerEventManager - marketToCreatedAt map[uint32]time.Time authorities map[string]struct{} currencyPairIDCache *CurrencyPairIDCache currencyPairIdCacheInitialized *atomic.Bool @@ -46,7 +44,6 @@ func NewKeeper( indexPriceCache: indexPriceCache, timeProvider: timeProvider, indexerEventManager: indexerEventManager, - marketToCreatedAt: map[uint32]time.Time{}, authorities: lib.UniqueSliceToSet(authorities), currencyPairIDCache: NewCurrencyPairIDCache(), currencyPairIdCacheInitialized: &atomic.Bool{}, // Initialized to false diff --git a/protocol/x/prices/keeper/market.go b/protocol/x/prices/keeper/market.go index d424ea582c..2399bf2e33 100644 --- a/protocol/x/prices/keeper/market.go +++ b/protocol/x/prices/keeper/market.go @@ -80,32 +80,11 @@ func (k Keeper) CreateMarket( ), ) - k.marketToCreatedAt[marketParam.Id] = k.timeProvider.Now() metrics.SetMarketPairForTelemetry(marketParam.Id, marketParam.Pair) return marketParam, nil } -// IsRecentlyAvailable returns true if the market was recently made available to the pricefeed daemon. A market is -// considered recently available either if it was recently created, or if the pricefeed daemon was recently started. If -// an index price does not exist for a recently available market, the protocol does not consider this an error -// condition, as it is expected that the pricefeed daemon will eventually provide a price for the market within a -// few seconds. -func (k Keeper) IsRecentlyAvailable(ctx sdk.Context, marketId uint32) bool { - createdAt, ok := k.marketToCreatedAt[marketId] - - if !ok { - return false - } - - // The comparison condition considers both market age and price daemon warmup time because a market can be - // created before or after the daemon starts. We use block height as a proxy for daemon warmup time because - // the price daemon is started when the gRPC service comes up, which typically occurs just before the first - // block is processed. - return k.timeProvider.Now().Sub(createdAt) < types.MarketIsRecentDuration || - ctx.BlockHeight() < types.PriceDaemonInitializationBlocks -} - // GetAllMarketParamPrices returns a slice of MarketParam, MarketPrice tuples for all markets. func (k Keeper) GetAllMarketParamPrices(ctx sdk.Context) ([]types.MarketParamPrice, error) { marketParams := k.GetAllMarketParams(ctx) diff --git a/protocol/x/prices/keeper/market_test.go b/protocol/x/prices/keeper/market_test.go index 35872d4091..410ad40c30 100644 --- a/protocol/x/prices/keeper/market_test.go +++ b/protocol/x/prices/keeper/market_test.go @@ -2,7 +2,6 @@ package keeper_test import ( "testing" - "time" errorsmod "cosmossdk.io/errors" "github.com/dydxprotocol/v4-chain/protocol/daemons/pricefeed/metrics" @@ -59,51 +58,6 @@ func TestCreateMarket(t *testing.T) { keepertest.AssertMarketCreateEventInIndexerBlock(t, keeper, ctx, marketParam) } -func TestMarketIsRecentlyAvailable(t *testing.T) { - tests := map[string]struct { - blockHeight int64 - now time.Time - expectedIsRecent bool - }{ - "Recent: << block height, << elapsed since market creation time": { - blockHeight: 0, - now: constants.TimeT.Add(types.MarketIsRecentDuration - 1), - expectedIsRecent: true, - }, - "Recent: >> block height, << elapsed since market creation time": { - blockHeight: types.PriceDaemonInitializationBlocks + 1, - now: constants.TimeT.Add(types.MarketIsRecentDuration - 1), - expectedIsRecent: true, - }, - "Recent: << block height, >> elapsed since market creation time": { - blockHeight: 0, - now: constants.TimeT.Add(types.MarketIsRecentDuration + 1), - expectedIsRecent: true, - }, - "Not recent: >> block height, >> elapsed since market creation time": { - blockHeight: types.PriceDaemonInitializationBlocks + 1, - now: constants.TimeT.Add(types.MarketIsRecentDuration + 1), - expectedIsRecent: false, - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - ctx, keeper, _, _, mockTimeProvider := keepertest.PricesKeepers(t) - - // Create market with TimeT creation timestamp. - mockTimeProvider.On("Now").Return(constants.TimeT).Once() - require.False(t, keeper.IsRecentlyAvailable(ctx, 0)) - - keepertest.CreateNMarkets(t, ctx, keeper, 1) - - ctx = ctx.WithBlockHeight(tc.blockHeight) - mockTimeProvider.On("Now").Return(tc.now).Once() - - require.Equal(t, tc.expectedIsRecent, keeper.IsRecentlyAvailable(ctx, 0)) - }) - } -} - func TestCreateMarket_Errors(t *testing.T) { validExchangeConfigJson := `{"exchanges":[{"exchangeName":"Binance","ticker":"BTCUSDT"}]}` tests := map[string]struct { diff --git a/protocol/x/prices/keeper/update_price.go b/protocol/x/prices/keeper/update_price.go index b4851b5c43..41c141ea7f 100644 --- a/protocol/x/prices/keeper/update_price.go +++ b/protocol/x/prices/keeper/update_price.go @@ -70,22 +70,9 @@ func (k Keeper) GetValidMarketPriceUpdates( // Skip proposal logic in the event of invalid inputs, which is only likely to occur around network genesis. if !indexPriceExists { metrics.IncrCountMetricWithLabels(types.ModuleName, metrics.IndexPriceDoesNotExist, marketMetricsLabel) - // Conditionally log missing index prices at least 20s after genesis/restart/market creation. We expect that - // there will be a delay in populating index prices after network genesis or a network restart, or when a - // market is created, it takes the daemon some time to warm up. - if !k.IsRecentlyAvailable(ctx, marketId) { - nonExistentMarkets = append(nonExistentMarkets, marketId) - } + nonExistentMarkets = append(nonExistentMarkets, marketId) continue } - if len(nonExistentMarkets) > 0 { - log.ErrorLog( - ctx, - "Index price for markets does not exist", - constants.MarketIdsLogKey, - nonExistentMarkets, - ) - } // Index prices of 0 are unexpected. In this scenario, we skip the proposal logic for the market and report an // error. @@ -130,6 +117,13 @@ func (k Keeper) GetValidMarketPriceUpdates( } } + if len(nonExistentMarkets) > 0 { + ctx.Logger().Warn( + "Index price for markets does not exist, marketIds: %v", + nonExistentMarkets, + ) + } + // 4. Sort price updates by market id in ascending order. sort.Slice(updates, func(i, j int) bool { return updates[i].MarketId < updates[j].MarketId }) diff --git a/protocol/x/prices/types/configs.go b/protocol/x/prices/types/configs.go index f00b5d9ae1..c4eed369c5 100644 --- a/protocol/x/prices/types/configs.go +++ b/protocol/x/prices/types/configs.go @@ -1,8 +1,5 @@ package types -import "time" - const ( - MarketIsRecentDuration = 20 * time.Second PriceDaemonInitializationBlocks = 20 )