Skip to content

Commit

Permalink
added stress test for order matching engine
Browse files Browse the repository at this point in the history
  • Loading branch information
faneaatiku committed Mar 11, 2024
1 parent c65add8 commit 4888753
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 28 deletions.
13 changes: 9 additions & 4 deletions testutil/simapp/simapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,24 @@ func (ao EmptyAppOptions) Get(o string) interface{} {
return nil
}

func setup(withGenesis bool, invCheckPeriod uint) (*SimApp, GenesisState) {
func setup(withGenesis bool, invCheckPeriod uint, withLogger bool) (*SimApp, GenesisState) {
db := dbm.NewMemDB()
encCdc := MakeEncodingConfig()
app := New(log.NewNopLogger(), db, nil, true, map[int64]bool{}, DefaultNodeHome, invCheckPeriod, encCdc, EmptyAppOptions{})
logger := log.NewNopLogger()
if withLogger {
logger = log.TestingLogger()
}

app := New(logger, db, nil, true, map[int64]bool{}, DefaultNodeHome, invCheckPeriod, encCdc, EmptyAppOptions{})
if withGenesis {
return app, NewDefaultGenesisState(encCdc.Marshaler)
}
return app, GenesisState{}
}

// Setup initializes a new SimApp. A Nop logger is set in SimApp.
func Setup(isCheckTx bool) *SimApp {
app, genesisState := setup(!isCheckTx, 5)
func Setup(isCheckTx, withLogger bool) *SimApp {
app, genesisState := setup(!isCheckTx, 5, withLogger)
if !isCheckTx {
// init chain must be called to stop deliverState from being nil
stateBytes, err := json.MarshalIndent(genesisState, "", " ")
Expand Down
3 changes: 3 additions & 0 deletions x/tradebin/keeper/calculator.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ func CalculateMinAmount(price string) sdk.Int {
fmt.Println("Error converting price to Dec:", err)
return sdk.NewInt(0)
}
if priceDec.IsZero() {
return sdk.NewInt(0)
}

// The denominator for our operation, represented as a Dec
oneDec := sdk.NewDec(1)
Expand Down
2 changes: 1 addition & 1 deletion x/tradebin/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type IntegrationTestSuite struct {
}

func (suite *IntegrationTestSuite) SetupTest() {
app := simapp.Setup(false)
app := simapp.Setup(false, false)
ctx := app.BaseApp.NewContext(false, tmproto.Header{Time: time.Now()})

suite.app = app
Expand Down
3 changes: 0 additions & 3 deletions x/tradebin/keeper/msg_server_create_order.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ func (k msgServer) CreateOrder(goCtx context.Context, msg *types.MsgCreateOrder)
}

k.SetQueueMessage(ctx, qm)

_ = ctx

err = k.emitOrderCreateMessageEvent(ctx, &qm)
if err != nil {
ctx.Logger().Error(err.Error())
Expand Down
158 changes: 158 additions & 0 deletions x/tradebin/keeper/msg_server_create_order_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package keeper_test

import (
"fmt"
"github.com/bze-alphateam/bze/testutil/simapp"
"github.com/bze-alphateam/bze/x/tradebin/keeper"
"github.com/bze-alphateam/bze/x/tradebin/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/tendermint/tendermint/libs/rand"
"strconv"
"time"
)

func (suite *IntegrationTestSuite) TestCreateOrder_InvalidAmount() {
Expand Down Expand Up @@ -259,3 +264,156 @@ func (suite *IntegrationTestSuite) TestCreateOrder_MarketTaker_Sell_Success() {
suite.Require().False(moduleBalance.IsZero())
suite.Require().Equal(moduleBalance.AmountOf(fee.Denom), fee.Amount)
}

func (suite *IntegrationTestSuite) TestCreateOrder_MarketTaker_StressBalance() {
suite.k.SetMarket(suite.ctx, market)
engine, err := keeper.NewProcessingEngine(suite.app.TradebinKeeper, suite.app.BankKeeper)
suite.Require().Nil(err)

//create initial random markets
testDenom1 := "test1"
testDenom2 := "test2"
market1 := types.Market{
Base: testDenom1,
Quote: testDenom2,
Creator: "addr1",
}
market2 := types.Market{
Base: denomStake,
Quote: testDenom2,
Creator: "addr1",
}
market3 := types.Market{
Base: denomBze,
Quote: testDenom1,
Creator: "addr1",
}

market4 := types.Market{
Base: denomBze,
Quote: testDenom2,
Creator: "addr1",
}
marketsMap := make(map[string]types.Market)
marketsMap[types.CreateMarketId(market.Base, market.Quote)] = market
marketsMap[types.CreateMarketId(market1.Base, market1.Quote)] = market1
marketsMap[types.CreateMarketId(market2.Base, market2.Quote)] = market2
marketsMap[types.CreateMarketId(market3.Base, market3.Quote)] = market3
marketsMap[types.CreateMarketId(market4.Base, market4.Quote)] = market4
suite.k.SetMarket(suite.ctx, market1)
suite.k.SetMarket(suite.ctx, market2)
suite.k.SetMarket(suite.ctx, market3)
suite.k.SetMarket(suite.ctx, market4)

//set initial balances to 10 random accounts
balances := sdk.NewCoins(
newStakeCoin(999999999999999),
newBzeCoin(999999999999999),
sdk.NewInt64Coin(testDenom1, 999999999999999),
sdk.NewInt64Coin(testDenom2, 999999999999999),
)
var creators []string
for i := 0; i < 10; i++ {
addr1 := sdk.AccAddress(fmt.Sprintf("addr%d_______________", i))
acc1 := suite.app.AccountKeeper.NewAccountWithAddress(suite.ctx, addr1)
suite.app.AccountKeeper.SetAccount(suite.ctx, acc1)
suite.Require().NoError(simapp.FundAccount(suite.app.BankKeeper, suite.ctx, addr1, balances))
creators = append(creators, addr1.String())
}

for i := 0; i < 5; i++ {
suite.randomOrderCreateMessages(suite.randomNumber(3000), creators, market)
suite.randomOrderCreateMessages(suite.randomNumber(3000), creators, market1)
suite.randomOrderCreateMessages(suite.randomNumber(3000), creators, market2)
suite.randomOrderCreateMessages(suite.randomNumber(3000), creators, market3)
suite.randomOrderCreateMessages(suite.randomNumber(3000), creators, market4)

engine.ProcessQueueMessages(suite.ctx)
}

allOrders := suite.k.GetAllOrder(suite.ctx)
suite.Require().NotEmpty(allOrders)
amounts := sdk.NewCoins(sdk.NewCoin(market.Base, sdk.ZeroInt()), sdk.NewCoin(market.Quote, sdk.ZeroInt()))
//check module balance is equal to the coins found
foundOrderTypesByPrice := make(map[string]string)
aggregatedOrders := make(map[string]types.AggregatedOrder)
for _, or := range allOrders {
//search to see if an opposite order with the same price already exists
//if so it means the engine failed to match them when they were processed
oppositeKey := fmt.Sprintf("%s_%s_%s", or.MarketId, or.Price, types.TheOtherOrderType(or.OrderType))
id, ok := foundOrderTypesByPrice[oppositeKey]
suite.Require().False(ok, fmt.Sprintf("order [%s] has the same price [%s] as [%s]", id, or.Price, or.Id))

//save locally to check later
key := fmt.Sprintf("%s_%s_%s", or.MarketId, or.Price, or.OrderType)
foundOrderTypesByPrice[key] = or.Id
amtInt, ok := sdk.NewIntFromString(or.Amount)
suite.Require().True(ok)
pickMarket, ok := marketsMap[or.MarketId]
suite.Require().True(ok)
coins, err := suite.k.GetOrderCoins(or.OrderType, or.Price, amtInt, &pickMarket)
suite.Require().NoError(err)

amounts = amounts.Add(coins)
//save in aggregated orders map so we can check it later
agg, found := aggregatedOrders[key]
if !found {
agg = types.AggregatedOrder{
MarketId: or.MarketId,
OrderType: or.OrderType,
Amount: "0",
Price: or.Price,
}
}
aggAmt, ok := sdk.NewIntFromString(agg.Amount)
suite.Require().True(ok)
aggAmt = aggAmt.Add(amtInt)
agg.Amount = aggAmt.String()
aggregatedOrders[key] = agg
}

for _, agg := range aggregatedOrders {
foundAgg, ok := suite.k.GetAggregatedOrder(suite.ctx, agg.MarketId, agg.OrderType, agg.Price)
suite.Require().True(ok)
suite.Require().Equal(foundAgg.Amount, agg.Amount)
}

moduleAddr := suite.app.AccountKeeper.GetModuleAddress(types.ModuleName)
moduleBalance := suite.app.BankKeeper.GetAllBalances(suite.ctx, moduleAddr)
suite.Require().Equal(moduleBalance, amounts)
}

func (suite *IntegrationTestSuite) randomOrderCreateMessages(count int, creators []string, market types.Market) []types.MsgCreateOrder {
var msgs []types.MsgCreateOrder
orderTypes := []string{types.OrderTypeBuy, types.OrderTypeSell}
goCtx := sdk.WrapSDKContext(suite.ctx)
for i := 0; i < count; i++ {
randomPrice := suite.randomNumber(4) + 1 //make sure it's always higher than 0
randomPriceStr := strconv.Itoa(randomPrice)
minAmount := keeper.CalculateMinAmount(randomPriceStr)
randomOrderType := orderTypes[i%2]
orderMsg := types.MsgCreateOrder{
Amount: minAmount.AddRaw(int64(suite.randomNumber(1000))).String(),
Price: randomPriceStr,
MarketId: types.CreateMarketId(market.Base, market.Quote),
OrderType: randomOrderType,
Creator: creators[suite.randomNumber(len(creators))],
}

_, err := suite.msgServer.CreateOrder(goCtx, &orderMsg)
suite.Require().NoError(err)
}

return msgs
}

func (suite *IntegrationTestSuite) randomNumber(to int) int {
// Seed the random number generator to get different results each run
// Uses the current time as the seed
rand.Seed(time.Now().UnixNano())

// Generate a random number between 0 and 99
num := rand.Intn(to)

return num
}
39 changes: 19 additions & 20 deletions x/tradebin/keeper/queue_message_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func (pe *ProcessingEngine) addOrder(ctx sdk.Context, message types.QueueMessage
aggAmountInt = aggAmountInt.Sub(amountToExecute)
agg.Amount = aggAmountInt.String()

err := pe.fundUsersAccounts(ctx, orderToFill, market, amountToExecute, msgOwnerAddr)
err := pe.fundUsersAccounts(ctx, &orderToFill, &market, amountToExecute, msgOwnerAddr)
if err != nil {
ctx.Logger().Error(fmt.Sprintf("[addOrder] %v", err))
return
Expand All @@ -202,27 +202,25 @@ func (pe *ProcessingEngine) addOrder(ctx sdk.Context, message types.QueueMessage
pe.k.RemoveOrder(ctx, orderToFill)
}

pe.addHistoryOrder(ctx, orderToFill, amountToExecute, message.Owner)
pe.addHistoryOrder(ctx, &orderToFill, amountToExecute, &message)
pe.emitOrderExecutedEvent(ctx, &orderToFill, amountToExecute.String())
}

ctx.Logger().Info("[addOrder] finished filling orders.")
if aggAmountInt.GT(zeroInt) {
pe.k.SetAggregatedOrder(ctx, agg)
ctx.Logger().Info("[addOrder] aggregated order updated")
} else {
pe.k.RemoveAggregatedOrder(ctx, agg)
ctx.Logger().Info("[addOrder] aggregated order removed")
}

if msgAmountInt.Equal(zeroInt) {
ctx.Logger().Info(fmt.Sprintf("[addOrder] message with id %s was completely filled", message.MessageId))
if aggAmountInt.GT(zeroInt) {
pe.k.SetAggregatedOrder(ctx, agg)
ctx.Logger().Info("[addOrder] aggregated order updated")
} else {
pe.k.RemoveAggregatedOrder(ctx, agg)
ctx.Logger().Info("[addOrder] aggregated order removed")
}
return
}
//if this code is reached then all orders are filled, and we have a remaining amount in the message to deal with
pe.k.RemoveAggregatedOrder(ctx, agg)

//if min amount condition is met we can place an order with the remaining message amount
if msgAmountInt.GTE(minAmount) {
//if min amount condition is met and all orders were filled we can proceed to place the order
if msgAmountInt.GTE(minAmount) && aggAmountInt.IsZero() {
ctx.Logger().Info(fmt.Sprintf("[addOrder] message with id %s has a remaining amount", message.MessageId))
order := pe.saveOrder(ctx, message, message.Amount)
pe.addOrderToAggregate(ctx, order)
Expand All @@ -246,14 +244,14 @@ func (pe *ProcessingEngine) addOrder(ctx sdk.Context, message types.QueueMessage
}
}

func (pe *ProcessingEngine) fundUsersAccounts(ctx sdk.Context, order types.Order, market types.Market, amount sdk.Int, taker sdk.AccAddress) error {
func (pe *ProcessingEngine) fundUsersAccounts(ctx sdk.Context, order *types.Order, market *types.Market, amount sdk.Int, taker sdk.AccAddress) error {
orderOwnerAddr, _ := sdk.AccAddressFromBech32(order.Owner)
coinsForOrderOwner, err := pe.k.GetOrderCoins(types.TheOtherOrderType(order.OrderType), order.Price, amount, &market)
coinsForOrderOwner, err := pe.k.GetOrderCoins(types.TheOtherOrderType(order.OrderType), order.Price, amount, market)
if err != nil {
return fmt.Errorf("error 1 when funding user accounts: %v", err)
}

coinsForMsgOwner, err := pe.k.GetOrderCoins(order.OrderType, order.Price, amount, &market)
coinsForMsgOwner, err := pe.k.GetOrderCoins(order.OrderType, order.Price, amount, market)
if err != nil {
return fmt.Errorf("error 2 when funding user accounts: %v", err)
}
Expand All @@ -268,21 +266,22 @@ func (pe *ProcessingEngine) fundUsersAccounts(ctx sdk.Context, order types.Order
return fmt.Errorf("error 4 when funding user accounts: %v", err)
}

ctx.Logger().Debug("[fundUsersAccounts] funded users accounts", amount.String(), order.Id)
return nil
}

func (pe *ProcessingEngine) addHistoryOrder(ctx sdk.Context, order types.Order, amount sdk.Int, taker string) {
func (pe *ProcessingEngine) addHistoryOrder(ctx sdk.Context, order *types.Order, amount sdk.Int, message *types.QueueMessage) {
history := types.HistoryOrder{
MarketId: order.MarketId,
OrderType: order.OrderType,
Amount: amount.String(),
Price: order.Price,
ExecutedAt: ctx.BlockTime().Unix(),
Maker: order.Owner,
Taker: taker,
Taker: message.Owner,
}

pe.k.SetHistoryOrder(ctx, history, order.Id)
pe.k.SetHistoryOrder(ctx, history, message.MessageId)
}

func (pe *ProcessingEngine) getExecutedAmount(messageAmount, orderAmount, minAmount sdk.Int) sdk.Int {
Expand Down

0 comments on commit 4888753

Please sign in to comment.