decred.org/dcrdex@v1.0.5/server/market/orderrouter.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package market 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/hex" 10 "errors" 11 "fmt" 12 "math" 13 "time" 14 15 "decred.org/dcrdex/dex" 16 "decred.org/dcrdex/dex/calc" 17 "decred.org/dcrdex/dex/msgjson" 18 "decred.org/dcrdex/dex/order" 19 "decred.org/dcrdex/dex/wait" 20 "decred.org/dcrdex/server/account" 21 "decred.org/dcrdex/server/asset" 22 "decred.org/dcrdex/server/comms" 23 "decred.org/dcrdex/server/matcher" 24 ) 25 26 // The AuthManager handles client-related actions, including authorization and 27 // communications. 28 type AuthManager interface { 29 Route(route string, handler func(account.AccountID, *msgjson.Message) *msgjson.Error) 30 Auth(user account.AccountID, msg, sig []byte) error 31 AcctStatus(user account.AccountID) (connected bool, tier int64) 32 Sign(...msgjson.Signable) 33 Send(account.AccountID, *msgjson.Message) error 34 Request(account.AccountID, *msgjson.Message, func(comms.Link, *msgjson.Message)) error 35 RequestWithTimeout(account.AccountID, *msgjson.Message, func(comms.Link, *msgjson.Message), time.Duration, func()) error 36 PreimageSuccess(user account.AccountID, refTime time.Time, oid order.OrderID) 37 MissedPreimage(user account.AccountID, refTime time.Time, oid order.OrderID) 38 RecordCancel(user account.AccountID, oid, target order.OrderID, epochGap int32, t time.Time) 39 RecordCompletedOrder(user account.AccountID, oid order.OrderID, t time.Time) 40 UserReputation(user account.AccountID) (tier int64, score, maxScore int32, err error) 41 } 42 43 const ( 44 maxClockOffset = 600_000 // milliseconds => 600 sec => 10 minutes 45 fundingTxWait = time.Minute 46 // ZeroConfFeeRateThreshold is multiplied by the last known fee rate for an 47 // asset to attain a minimum fee rate acceptable for zero-conf funding 48 // coins. 49 ZeroConfFeeRateThreshold = 0.9 50 ) 51 52 // MarketTunnel is a connection to a market. 53 type MarketTunnel interface { 54 // SubmitOrder submits the order to the market for insertion into the epoch 55 // queue. 56 SubmitOrder(*orderRecord) error 57 // MidGap returns the mid-gap market rate, which is ths rate halfway between 58 // the best buy order and the best sell order in the order book. 59 MidGap() uint64 60 // MarketBuyBuffer is a coefficient that when multiplied by the market's lot 61 // size specifies the minimum required amount for a market buy order. 62 MarketBuyBuffer() float64 63 // LotSize is the market's lot size in units of the base asset. 64 LotSize() uint64 65 // RateStep is the market's rate step in units of the quote asset. 66 RateStep() uint64 67 // CoinLocked should return true if the CoinID is currently a funding Coin 68 // for an active DEX order. This is required for Coin validation to prevent 69 // a user from submitting multiple orders spending the same Coin. This 70 // method will likely need to check all orders currently in the epoch queue, 71 // the order book, and the swap monitor, since UTXOs will still be unspent 72 // according to the asset backends until the client broadcasts their 73 // initialization transaction. 74 // 75 // DRAFT NOTE: This function could also potentially be handled by persistent 76 // storage, since active orders and active matches are tracked there. 77 CoinLocked(assetID uint32, coinID order.CoinID) bool 78 // Cancelable determines whether an order is cancelable. A cancelable order 79 // is a limit order with time-in-force standing either in the epoch queue or 80 // in the order book. 81 Cancelable(order.OrderID) bool 82 83 // Suspend suspends the market as soon as a given time, returning the final 84 // epoch index and and time at which that epoch closes. 85 Suspend(asSoonAs time.Time, persistBook bool) (finalEpochIdx int64, finalEpochEnd time.Time) 86 87 // Running indicates is the market is accepting new orders. This will return 88 // false when suspended, but false does not necessarily mean Run has stopped 89 // since a start epoch may be set. 90 Running() bool 91 92 // CheckUnfilled checks a user's unfilled book orders that are funded by 93 // coins for a given asset to ensure that their funding coins are not spent. 94 // If any of an unfilled order's funding coins are spent, the order is 95 // unbooked (removed from the in-memory book, revoked in the DB, a 96 // cancellation marked against the user, coins unlocked, and orderbook 97 // subscribers notified). See Unbook for details. 98 CheckUnfilled(assetID uint32, user account.AccountID) (unbooked []*order.LimitOrder) 99 100 // Parcels calculates the number of active parcels for the market. 101 Parcels(user account.AccountID, settlingQty uint64) float64 102 } 103 104 type MarketParcelCalculator func(settlingQty uint64) (parcels float64) 105 106 // orderRecord contains the information necessary to respond to an order 107 // request. 108 type orderRecord struct { 109 order order.Order 110 req msgjson.Stampable 111 msgID uint64 112 } 113 114 // assetSet is pointers to two different assets, but with 4 ways of addressing 115 // them. 116 type assetSet struct { 117 funding *asset.BackedAsset 118 receiving *asset.BackedAsset 119 base *asset.BackedAsset 120 quote *asset.BackedAsset 121 } 122 123 // newAssetSet is a constructor for an assetSet. 124 func newAssetSet(base, quote *asset.BackedAsset, sell bool) *assetSet { 125 coins := &assetSet{ 126 quote: quote, 127 base: base, 128 funding: quote, 129 receiving: base, 130 } 131 if sell { 132 coins.funding, coins.receiving = base, quote 133 } 134 return coins 135 } 136 137 // FeeSource is a source of the last reported tx fee rate estimate for an asset. 138 type FeeSource interface { 139 LastRate(assetID uint32) (feeRate uint64) 140 } 141 142 // MatchSwapper is a source for information about settling matches. 143 type MatchSwapper interface { 144 UnsettledQuantity(user account.AccountID) map[[2]uint32]uint64 145 } 146 147 // OrderRouter handles the 'limit', 'market', and 'cancel' DEX routes. These 148 // are authenticated routes used for placing and canceling orders. 149 type OrderRouter struct { 150 auth AuthManager 151 assets map[uint32]*asset.BackedAsset 152 tunnels map[string]MarketTunnel 153 latencyQ *wait.TickerQueue 154 feeSource FeeSource 155 dexBalancer *DEXBalancer 156 swapper MatchSwapper 157 } 158 159 // OrderRouterConfig is the configuration settings for an OrderRouter. 160 type OrderRouterConfig struct { 161 AuthManager AuthManager 162 Assets map[uint32]*asset.BackedAsset 163 Markets map[string]MarketTunnel 164 FeeSource FeeSource 165 DEXBalancer *DEXBalancer 166 MatchSwapper MatchSwapper 167 } 168 169 // NewOrderRouter is a constructor for an OrderRouter. 170 func NewOrderRouter(cfg *OrderRouterConfig) *OrderRouter { 171 router := &OrderRouter{ 172 auth: cfg.AuthManager, 173 assets: cfg.Assets, 174 tunnels: cfg.Markets, 175 latencyQ: wait.NewTickerQueue(2 * time.Second), 176 feeSource: cfg.FeeSource, 177 dexBalancer: cfg.DEXBalancer, 178 swapper: cfg.MatchSwapper, 179 } 180 cfg.AuthManager.Route(msgjson.LimitRoute, router.handleLimit) 181 cfg.AuthManager.Route(msgjson.MarketRoute, router.handleMarket) 182 cfg.AuthManager.Route(msgjson.CancelRoute, router.handleCancel) 183 return router 184 } 185 186 func (r *OrderRouter) Run(ctx context.Context) { 187 r.latencyQ.Run(ctx) 188 } 189 190 func (r *OrderRouter) respondError(reqID uint64, user account.AccountID, msgErr *msgjson.Error) { 191 log.Debugf("Error going to user %v: %s", user, msgErr) 192 msg, err := msgjson.NewResponse(reqID, nil, msgErr) 193 if err != nil { 194 log.Errorf("Failed to create error response with message '%s': %v", msg, err) 195 return // this should not be possible, but don't pass nil msg to Send 196 } 197 if err := r.auth.Send(user, msg); err != nil { 198 log.Infof("Failed to send %s error response (msg = %s) to disconnected user %v: %q", 199 msg.Route, msgErr, user, err) 200 } 201 } 202 203 func fundingCoin(backend asset.Backend, coinID []byte, redeemScript []byte) (asset.FundingCoin, error) { 204 outputTracker, is := backend.(asset.OutputTracker) 205 if !is { 206 return nil, fmt.Errorf("fundingCoin requested for incapable asset") 207 } 208 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 209 defer cancel() 210 return outputTracker.FundingCoin(ctx, coinID, redeemScript) 211 } 212 213 func coinConfirmations(coin asset.Coin) (int64, error) { 214 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 215 defer cancel() 216 return coin.Confirmations(ctx) 217 } 218 219 // handleLimit is the handler for the 'limit' route. This route accepts a 220 // msgjson.Limit payload, validates the information, constructs an 221 // order.LimitOrder and submits it to the epoch queue. 222 func (r *OrderRouter) handleLimit(user account.AccountID, msg *msgjson.Message) *msgjson.Error { 223 limit := new(msgjson.LimitOrder) 224 err := msg.Unmarshal(&limit) 225 if err != nil || limit == nil { 226 return msgjson.NewError(msgjson.RPCParseError, "error decoding 'limit' payload") 227 } 228 229 rpcErr := r.verifyAccount(user, limit.AccountID, limit) 230 if rpcErr != nil { 231 return rpcErr 232 } 233 234 if _, tier := r.auth.AcctStatus(user); tier < 1 { 235 return msgjson.NewError(msgjson.AccountClosedError, "account %v with tier %d may not submit trade orders", user, tier) 236 } 237 238 tunnel, assets, sell, rpcErr := r.extractMarketDetails(&limit.Prefix, &limit.Trade) 239 if rpcErr != nil { 240 return rpcErr 241 } 242 243 // Spare some resources if the market is closed now. Any orders that make it 244 // through to a closed market will receive a similar error from SubmitOrder. 245 if !tunnel.Running() { 246 return msgjson.NewError(msgjson.MarketNotRunningError, "market closed to new orders") 247 } 248 249 // Check that OrderType is set correctly 250 if limit.OrderType != msgjson.LimitOrderNum { 251 return msgjson.NewError(msgjson.OrderParameterError, "wrong order type set for limit order. wanted %d, got %d", 252 msgjson.LimitOrderNum, limit.OrderType) 253 } 254 255 // Check that the rate is non-zero and obeys the rate step interval. 256 if limit.Rate == 0 { 257 return msgjson.NewError(msgjson.OrderParameterError, "rate = 0 not allowed") 258 } 259 if rateStep := tunnel.RateStep(); limit.Rate%rateStep != 0 { 260 return msgjson.NewError(msgjson.OrderParameterError, "rate (%d) not a multiple of ratestep (%d)", 261 limit.Rate, rateStep) 262 } 263 264 // Check time-in-force 265 var force order.TimeInForce 266 switch limit.TiF { 267 case msgjson.StandingOrderNum: 268 force = order.StandingTiF 269 case msgjson.ImmediateOrderNum: 270 force = order.ImmediateTiF 271 default: 272 return msgjson.NewError(msgjson.OrderParameterError, "unknown time-in-force") 273 } 274 275 lotSize := tunnel.LotSize() 276 rpcErr = r.checkPrefixTrade(assets, lotSize, &limit.Prefix, &limit.Trade, true) 277 if rpcErr != nil { 278 return rpcErr 279 } 280 281 // Commitment 282 if len(limit.Commit) != order.CommitmentSize { 283 return msgjson.NewError(msgjson.OrderParameterError, "invalid commitment") 284 } 285 var commit order.Commitment 286 copy(commit[:], limit.Commit) 287 288 coinIDs := make([]order.CoinID, 0, len(limit.Trade.Coins)) 289 for _, coin := range limit.Trade.Coins { 290 coinID := order.CoinID(coin.ID) 291 coinIDs = append(coinIDs, coinID) 292 } 293 294 // Create the limit order. 295 lo := &order.LimitOrder{ 296 P: order.Prefix{ 297 AccountID: user, 298 BaseAsset: limit.Base, 299 QuoteAsset: limit.Quote, 300 OrderType: order.LimitOrderType, 301 ClientTime: time.UnixMilli(int64(limit.ClientTime)), 302 //ServerTime set in epoch queue processing pipeline. 303 Commit: commit, 304 }, 305 T: order.Trade{ 306 Coins: coinIDs, 307 Sell: sell, 308 Quantity: limit.Quantity, 309 Address: limit.Address, 310 }, 311 Rate: limit.Rate, 312 Force: force, 313 } 314 315 // NOTE: ServerTime is not yet set, so the order's ID, which is computed 316 // from the serialized order, is not yet valid. The Market will stamp the 317 // order on receipt, and the order ID will be valid. 318 319 oRecord := &orderRecord{ 320 order: lo, 321 req: limit, 322 msgID: msg.ID, 323 } 324 325 return r.processTrade(oRecord, tunnel, assets, limit.Coins, sell, limit.Rate, limit.RedeemSig, limit.Serialize()) 326 } 327 328 // handleMarket is the handler for the 'market' route. This route accepts a 329 // msgjson.MarketOrder payload, validates the information, constructs an 330 // order.MarketOrder and submits it to the epoch queue. 331 func (r *OrderRouter) handleMarket(user account.AccountID, msg *msgjson.Message) *msgjson.Error { 332 market := new(msgjson.MarketOrder) 333 err := msg.Unmarshal(&market) 334 if err != nil || market == nil { 335 return msgjson.NewError(msgjson.RPCParseError, "error decoding 'market' payload") 336 } 337 338 rpcErr := r.verifyAccount(user, market.AccountID, market) 339 if rpcErr != nil { 340 return rpcErr 341 } 342 343 if _, tier := r.auth.AcctStatus(user); tier < 1 { 344 return msgjson.NewError(msgjson.AccountClosedError, "account %v with tier %d may not submit trade orders", user, tier) 345 } 346 347 tunnel, assets, sell, rpcErr := r.extractMarketDetails(&market.Prefix, &market.Trade) 348 if rpcErr != nil { 349 return rpcErr 350 } 351 352 if !tunnel.Running() { 353 mktName, _ := dex.MarketName(market.Base, market.Quote) 354 return msgjson.NewError(msgjson.MarketNotRunningError, "market %s closed to new orders", mktName) 355 } 356 357 // Check that OrderType is set correctly 358 if market.OrderType != msgjson.MarketOrderNum { 359 return msgjson.NewError(msgjson.OrderParameterError, "wrong order type set for market order") 360 } 361 362 // Passing sell as the checkLot parameter causes the lot size check to be 363 // ignored for market buy orders. 364 lotSize := tunnel.LotSize() 365 rpcErr = r.checkPrefixTrade(assets, lotSize, &market.Prefix, &market.Trade, sell) 366 if rpcErr != nil { 367 return rpcErr 368 } 369 370 // Commitment. 371 if len(market.Commit) != order.CommitmentSize { 372 return msgjson.NewError(msgjson.OrderParameterError, "invalid commitment") 373 } 374 var commit order.Commitment 375 copy(commit[:], market.Commit) 376 377 coinIDs := make([]order.CoinID, 0, len(market.Trade.Coins)) 378 for _, coin := range market.Trade.Coins { 379 coinID := order.CoinID(coin.ID) 380 coinIDs = append(coinIDs, coinID) 381 } 382 383 // Create the market order 384 mo := &order.MarketOrder{ 385 P: order.Prefix{ 386 AccountID: user, 387 BaseAsset: market.Base, 388 QuoteAsset: market.Quote, 389 OrderType: order.MarketOrderType, 390 ClientTime: time.UnixMilli(int64(market.ClientTime)), 391 //ServerTime set in epoch queue processing pipeline. 392 Commit: commit, 393 }, 394 T: order.Trade{ 395 Coins: coinIDs, 396 Sell: sell, 397 Quantity: market.Quantity, 398 Address: market.Address, 399 }, 400 } 401 402 // Send the order to the epoch queue. 403 oRecord := &orderRecord{ 404 order: mo, 405 req: market, 406 msgID: msg.ID, 407 } 408 409 return r.processTrade(oRecord, tunnel, assets, market.Coins, sell, 0, market.RedeemSig, market.Serialize()) 410 } 411 412 // processTrade checks that the trade is valid and submits it to the market. 413 func (r *OrderRouter) processTrade(oRecord *orderRecord, tunnel MarketTunnel, assets *assetSet, 414 coins []*msgjson.Coin, sell bool, rate uint64, redeemSig *msgjson.RedeemSig, sigMsg []byte) *msgjson.Error { 415 416 fundingAsset := assets.funding 417 user := oRecord.order.User() 418 trade := oRecord.order.Trade() 419 420 // If the receiving asset is account-based, we need to check that they can 421 // cover fees for the redemption, since they can't be subtracted from the 422 // received amount. 423 receivingBalancer, isToAccount := assets.receiving.Backend.(asset.AccountBalancer) 424 if isToAccount { 425 if redeemSig == nil { 426 log.Infof("user %s did not include a RedeemSig for received asset %s", user, assets.receiving.Symbol) 427 return msgjson.NewError(msgjson.OrderParameterError, "no redeem address verification included for asset %s", assets.receiving.Symbol) 428 } 429 430 acctAddr := trade.ToAccount() 431 if err := receivingBalancer.ValidateSignature(acctAddr, redeemSig.PubKey, sigMsg, redeemSig.Sig); err != nil { 432 log.Infof("user %s failed redeem signature validation for order: %v", 433 user, err) 434 return msgjson.NewError(msgjson.SignatureError, "redeem signature validation failed") 435 } 436 437 if !r.sufficientAccountBalance(acctAddr, oRecord.order, assets.receiving.Asset.ID, assets.receiving.ID, tunnel) { 438 return msgjson.NewError(msgjson.FundingError, "insufficient balance") 439 } 440 } 441 442 // If the funding asset is account-based, we'll check balance and submit the 443 // order immediately, since we don't need to find coins. 444 fundingBalancer, isAccountFunded := assets.funding.Backend.(asset.AccountBalancer) 445 if isAccountFunded { 446 // Validate that the coins are correct for an account-based-asset-funded 447 // order. There should be 1 coin, 1 sig, 1 pubkey, and no redeem script. 448 if len(coins) != 1 { 449 log.Infof("user %s submitted an %s-funded order with %d coin IDs", user, assets.funding.Symbol, len(coins)) 450 return msgjson.NewError(msgjson.OrderParameterError, "account-type asset funding requires exactly one coin ID") 451 } 452 acctProof := coins[0] 453 if len(acctProof.PubKeys) != 1 || len(acctProof.Sigs) != 1 || len(acctProof.Redeem) > 0 { 454 log.Infof("user %s submitted an %s-funded order with %d pubkeys, %d sigs, redeem script length %d", 455 user, assets.funding.Symbol, len(acctProof.PubKeys), len(acctProof.Sigs), len(acctProof.Redeem)) 456 return msgjson.NewError(msgjson.OrderParameterError, "account-type asset funding requires exactly one coin ID") 457 } 458 459 acctAddr := trade.FromAccount() 460 pubKey := acctProof.PubKeys[0] 461 sig := acctProof.Sigs[0] 462 if err := fundingBalancer.ValidateSignature(acctAddr, pubKey, sigMsg, sig); err != nil { 463 log.Infof("user %s failed signature validation for order: %v", 464 user, err) 465 return msgjson.NewError(msgjson.SignatureError, "signature validation failed") 466 } 467 468 if !r.sufficientAccountBalance(acctAddr, oRecord.order, assets.funding.Asset.ID, assets.receiving.ID, tunnel) { 469 return msgjson.NewError(msgjson.FundingError, "insufficient balance") 470 } 471 return r.submitOrderToMarket(tunnel, oRecord) 472 } 473 474 // Funding coins are from a utxo-based asset. Need to find them. 475 476 funder, is := assets.funding.Backend.(asset.OutputTracker) 477 if !is { 478 return msgjson.NewError(msgjson.RPCInternal, "internal error") 479 } 480 481 // Validate coin IDs and prepare some strings for debug logging. 482 coinStrs := make([]string, 0, len(coins)) 483 for _, coinID := range trade.Coins { 484 coinStr, err := fundingAsset.Backend.ValidateCoinID(coinID) 485 if err != nil { 486 return msgjson.NewError(msgjson.FundingError, "invalid coin ID %v: %v", coinID, err) 487 } 488 // TODO: Check all markets here? 489 if tunnel.CoinLocked(assets.funding.ID, coinID) { 490 return msgjson.NewError(msgjson.FundingError, "coin %s is locked", fmtCoinID(assets.funding.ID, coinID)) 491 } 492 coinStrs = append(coinStrs, coinStr) 493 } 494 495 // Use this as a chance to check user's existing market orders. 496 // TODO: check all markets? 497 for mktName, tunnel := range r.tunnels { 498 unbookedUnfunded := tunnel.CheckUnfilled(assets.funding.ID, oRecord.order.User()) 499 for _, badLo := range unbookedUnfunded { 500 log.Infof("Unbooked unfunded order %v from market %s for user %v", badLo, mktName, oRecord.order.User()) 501 } 502 } 503 504 lotSize := tunnel.LotSize() 505 506 midGap := tunnel.MidGap() 507 if midGap == 0 { 508 midGap = tunnel.RateStep() 509 } 510 511 lots := trade.Quantity / lotSize 512 if !sell && rate == 0 { 513 lots = matcher.QuoteToBase(midGap, trade.Quantity) / lotSize 514 } 515 516 var valSum uint64 517 var spendSize uint32 518 neededCoins := make(map[int]*msgjson.Coin, len(trade.Coins)) 519 for i, coin := range coins { 520 neededCoins[i] = coin 521 } 522 523 checkCoins := func() (tryAgain bool, msgErr *msgjson.Error) { 524 for key, coin := range neededCoins { 525 // Get the coin from the backend and validate it. 526 dexCoin, err := fundingCoin(fundingAsset.Backend, coin.ID, coin.Redeem) 527 if err != nil { 528 if errors.Is(err, asset.CoinNotFoundError) { 529 return true, nil 530 } 531 if errors.Is(err, asset.ErrRequestTimeout) { 532 log.Errorf("Deadline exceeded attempting to verify funding coin %v (%s). Will try again.", 533 coin.ID, fundingAsset.Symbol) 534 return true, nil 535 } 536 log.Errorf("Error retrieving limit order funding coin ID %s. user = %s: %v", coin.ID, user, err) 537 return false, msgjson.NewError(msgjson.FundingError, "error retrieving coin ID %v", coin.ID) 538 } 539 540 // Verify that the user controls the funding coins. 541 err = dexCoin.Auth(msgBytesToBytes(coin.PubKeys), msgBytesToBytes(coin.Sigs), coin.ID) 542 if err != nil { 543 log.Debugf("Auth error for %s coin %s: %v", fundingAsset.Symbol, dexCoin, err) 544 return false, msgjson.NewError(msgjson.CoinAuthError, "failed to authorize coin %v", dexCoin) 545 } 546 547 msgErr := r.checkZeroConfs(dexCoin, fundingAsset) 548 if msgErr != nil { 549 return false, msgErr 550 } 551 552 delete(neededCoins, key) // don't check this coin again 553 valSum += dexCoin.Coin().Value() 554 // NOTE: Summing like this is actually not quite sufficient to 555 // estimate the size associated with the input, because if it's a 556 // BTC segwit output, we would also have to account for the marker 557 // and flag weight, but only once per tx. The weight would add 558 // either 0 or 1 byte to the tx virtual size, so we have a chance of 559 // under-estimating by 1 byte to the advantage of the client. It 560 // won't ever cause issues though, because we also require funding 561 // for a change output in the final swap, which is actually not 562 // needed, so there's some buffer. 563 spendSize += dexCoin.SpendSize() 564 } 565 566 if valSum == 0 { 567 return false, msgjson.NewError(msgjson.FundingError, "zero value funding coins not permitted") 568 } 569 570 // Calculate the fees and check that the utxo sum is enough. 571 var swapVal uint64 572 if sell { 573 swapVal = trade.Quantity 574 } else { 575 if rate > 0 { // limit buy 576 swapVal = calc.BaseToQuote(rate, trade.Quantity) 577 } else { 578 // This is a market buy order, so the quantity gets special handling. 579 // 1. The quantity is in units of the quote asset. 580 // 2. The quantity has to satisfy the market buy buffer. 581 midGap := tunnel.MidGap() 582 if midGap == 0 { 583 midGap = tunnel.RateStep() 584 } 585 buyBuffer := tunnel.MarketBuyBuffer() 586 lotWithBuffer := uint64(float64(lotSize) * buyBuffer) 587 swapVal = matcher.BaseToQuote(midGap, lotWithBuffer) 588 if trade.Quantity < swapVal { 589 return false, msgjson.NewError(msgjson.FundingError, "order quantity does not satisfy market buy buffer. %d < %d. midGap = %d", 590 trade.Quantity, swapVal, midGap) 591 } 592 } 593 } 594 595 if !funder.ValidateOrderFunding(swapVal, valSum, uint64(len(trade.Coins)), uint64(spendSize), lots, &assets.funding.Asset) { 596 return false, msgjson.NewError(msgjson.FundingError, "failed funding validation") 597 } 598 599 return false, nil 600 } 601 602 log.Tracef("Searching for %s coins %v for new order", fundingAsset.Symbol, coinStrs) 603 r.latencyQ.Wait(&wait.Waiter{ 604 Expiration: time.Now().Add(fundingTxWait), 605 TryFunc: func() wait.TryDirective { 606 tryAgain, msgErr := checkCoins() 607 if tryAgain { 608 return wait.TryAgain 609 } 610 if msgErr != nil { 611 r.respondError(oRecord.msgID, user, msgErr) 612 return wait.DontTryAgain 613 } 614 615 // Send the order to the epoch queue where it will be time stamped. 616 log.Tracef("Found and validated %s coins %v for new order", fundingAsset.Symbol, coinStrs) 617 if msgErr := r.submitOrderToMarket(tunnel, oRecord); msgErr != nil { 618 r.respondError(oRecord.msgID, user, msgErr) 619 } 620 return wait.DontTryAgain 621 }, 622 ExpireFunc: func() { 623 // Tell them to broadcast again or check their node before broadcast 624 // timeout is reached and the match is revoked. 625 r.respondError(oRecord.msgID, user, msgjson.NewError(msgjson.TransactionUndiscovered, 626 "failed to find funding coins %v", coinStrs)) 627 }, 628 }) 629 630 return nil 631 } 632 633 // sufficientAccountBalance checks that the user's account-based asset balance 634 // is sufficient to support the order, considering the user's other orders and 635 // active matches across all DEX markets. 636 func (r *OrderRouter) sufficientAccountBalance(accountAddr string, ord order.Order, 637 assetID, redeemAssetID uint32, tunnel MarketTunnel) bool { 638 trade := ord.Trade() 639 640 // This asset is funding an order when it is either: 641 // - base asset in a sell order e.g. selling ETH in a ETH-LTC market 642 // - quote asset in a buy order e.g. buying BTC in a BTC-ETH market 643 // This asset will be redeemed when it is either: 644 // - base asset in a buy order e.g. buying ETH in a ETH-LTC market 645 // - quote asset in a sell order e.g. selling in a BTC-ETH market 646 647 var fundingQty, fundingLots uint64 // when the asset is base in sell order, or quote in buy order 648 var redeems int // when the asset is base in buy order, or quote in sell order 649 if ord.Base() == assetID { 650 if trade.Sell { 651 fundingQty = trade.Quantity 652 fundingLots = trade.Quantity / tunnel.LotSize() 653 } else { // buying base asset 654 baseQty := trade.Quantity 655 if _, ok := ord.(*order.MarketOrder); ok { 656 // Market buy Quantity is in units of quote asset, so estimate 657 // how much of base asset that might be based on mid-gap rate. 658 baseQty = calc.QuoteToBase(safeMidGap(tunnel), trade.Quantity) 659 } 660 redeems = int(baseQty / tunnel.LotSize()) 661 } 662 } else { 663 if trade.Sell { 664 redeems = int(trade.Quantity / tunnel.LotSize()) 665 } else { 666 if lo, ok := ord.(*order.LimitOrder); ok { 667 fundingQty = calc.BaseToQuote(lo.Rate, trade.Quantity) 668 fundingLots = trade.Quantity / tunnel.LotSize() 669 } else { // market buy 670 fundingQty = trade.Quantity 671 fundingLots = fundingQty / tunnel.LotSize() 672 } 673 } 674 } 675 676 return r.dexBalancer.CheckBalance(accountAddr, assetID, redeemAssetID, fundingQty, fundingLots, redeems) 677 } 678 679 // calcParcelLimit computes the users score-scaled user parcel limit. 680 func calcParcelLimit(tier int64, score, maxScore int32) uint32 { 681 // Users limit starts at 2 parcels per tier. 682 lowerLimit := tier * dex.PerTierBaseParcelLimit 683 // Limit can scale up to 3x with score. 684 upperLimit := lowerLimit * dex.ParcelLimitScoreMultiplier 685 limitRange := upperLimit - lowerLimit 686 var scaleFactor float64 687 if score > 0 { 688 scaleFactor = float64(score) / float64(maxScore) 689 } 690 return uint32(lowerLimit) + uint32(math.Round(scaleFactor*float64(limitRange))) 691 } 692 693 // CheckParcelLimit checks that the user does not exceed their parcel limit. 694 // The calcParcels function must be provided by the order's targeted Market, and 695 // calculate the number of parcels from that market when quantity from settling 696 // matches is taken into consideration. CheckParcelLimit checks the global 697 // parcel limit, based on the users tier and score and active orders for ALL 698 // markets. 699 func (r *OrderRouter) CheckParcelLimit(user account.AccountID, targetMarketName string, calcParcels MarketParcelCalculator) bool { 700 tier, score, maxScore, err := r.auth.UserReputation(user) 701 if err != nil { 702 log.Errorf("error getting user score for parcel limit check: %w", err) 703 return false 704 } 705 if tier <= 0 { 706 return false 707 } 708 709 roundParcels := func(parcels float64) uint32 { 710 // Rounding to 8 decimal places first should resolve any floating point 711 // error, then we take the floor. 1e8 is not completetly arbitrary. We 712 // need to choose a number of decimals of an order > the expected parcel 713 // size of a low-lot-size market, which I expect wouldn't be greater 714 // than 1e5. 715 return uint32(math.Round(parcels*1e8) / 1e8) 716 } 717 718 parcelLimit := calcParcelLimit(tier, score, maxScore) 719 720 settlingQuantities := make(map[string]uint64) 721 for bq, qty := range r.swapper.UnsettledQuantity(user) { 722 mktName, _ := dex.MarketName(bq[0], bq[1]) 723 settlingQuantities[mktName] += qty 724 } 725 726 var otherMarketParcels float64 727 var settlingQty uint64 728 for mktName, mkt := range r.tunnels { 729 if mktName == targetMarketName { 730 settlingQty = settlingQuantities[mktName] 731 continue 732 } 733 734 otherMarketParcels += mkt.Parcels(user, settlingQuantities[mktName]) 735 if roundParcels(otherMarketParcels) > parcelLimit { 736 return false 737 } 738 } 739 targetMarketParcels := calcParcels(settlingQty) 740 741 return roundParcels(otherMarketParcels+targetMarketParcels) <= parcelLimit 742 } 743 744 func (r *OrderRouter) submitOrderToMarket(tunnel MarketTunnel, oRecord *orderRecord) *msgjson.Error { 745 if err := tunnel.SubmitOrder(oRecord); err != nil { 746 code := msgjson.UnknownMarketError 747 switch { 748 case errors.Is(err, ErrInternalServer): 749 log.Errorf("Market failed to SubmitOrder: %v", err) 750 case errors.Is(err, ErrQuantityTooHigh): 751 code = msgjson.OrderQuantityTooHigh 752 fallthrough 753 default: 754 log.Debugf("Market failed to SubmitOrder: %v", err) 755 } 756 return msgjson.NewError(code, "%v", err) 757 } 758 return nil 759 } 760 761 // Check the FundingCoin confirmations, and if zero, ensure the tx fee rate 762 // is sufficient, > 90% of our last recorded estimate for the asset. 763 func (r *OrderRouter) checkZeroConfs(dexCoin asset.FundingCoin, fundingAsset *asset.BackedAsset) *msgjson.Error { 764 // Verify that zero-conf coins are within 10% of the last known fee 765 // rate. 766 confs, err := coinConfirmations(dexCoin.Coin()) 767 if err != nil { 768 log.Debugf("Confirmations error for %s coin %s: %v", fundingAsset.Symbol, dexCoin, err) 769 return msgjson.NewError(msgjson.FundingError, "failed to verify coin %v", dexCoin) 770 } 771 if confs > 0 { 772 return nil 773 } 774 lastKnownFeeRate := r.feeSource.LastRate(fundingAsset.ID) // MaxFeeRate applied inside feeSource 775 feeMinimum := uint64(math.Round(float64(lastKnownFeeRate) * ZeroConfFeeRateThreshold)) 776 777 if !fundingAsset.Backend.ValidateFeeRate(dexCoin.Coin(), feeMinimum) { 778 log.Debugf("Fees too low %s coin %s: fee mim %d", fundingAsset.Symbol, dexCoin, feeMinimum) 779 return msgjson.NewError(msgjson.FundingError, 780 "fee rate for %s is too low. fee min %d", dexCoin, feeMinimum) 781 } 782 return nil 783 } 784 785 // handleCancel is the handler for the 'cancel' route. This route accepts a 786 // msgjson.Cancel payload, validates the information, constructs an 787 // order.CancelOrder and submits it to the epoch queue. 788 func (r *OrderRouter) handleCancel(user account.AccountID, msg *msgjson.Message) *msgjson.Error { 789 cancel := new(msgjson.CancelOrder) 790 err := msg.Unmarshal(&cancel) 791 if err != nil || cancel == nil { 792 return msgjson.NewError(msgjson.RPCParseError, "error decoding 'cancel' payload") 793 } 794 795 rpcErr := r.verifyAccount(user, cancel.AccountID, cancel) 796 if rpcErr != nil { 797 return rpcErr 798 } 799 800 // NOTE: Allow suspended accounts to submit cancel orders. 801 802 tunnel, rpcErr := r.extractMarket(&cancel.Prefix) 803 if rpcErr != nil { 804 return rpcErr 805 } 806 807 if len(cancel.TargetID) != order.OrderIDSize { 808 return msgjson.NewError(msgjson.OrderParameterError, "invalid target ID format") 809 } 810 var targetID order.OrderID 811 copy(targetID[:], cancel.TargetID) 812 813 if !tunnel.Cancelable(targetID) { 814 return msgjson.NewError(msgjson.OrderParameterError, "target order not known: %v", targetID) 815 } 816 817 // Check that OrderType is set correctly 818 if cancel.OrderType != msgjson.CancelOrderNum { 819 return msgjson.NewError(msgjson.OrderParameterError, "wrong order type set for cancel order") 820 } 821 822 rpcErr = checkTimes(&cancel.Prefix) 823 if rpcErr != nil { 824 return rpcErr 825 } 826 827 // Commitment. 828 if len(cancel.Commit) != order.CommitmentSize { 829 return msgjson.NewError(msgjson.OrderParameterError, "invalid commitment") 830 } 831 var commit order.Commitment 832 copy(commit[:], cancel.Commit) 833 834 // Create the cancel order 835 co := &order.CancelOrder{ 836 P: order.Prefix{ 837 AccountID: user, 838 BaseAsset: cancel.Base, 839 QuoteAsset: cancel.Quote, 840 OrderType: order.CancelOrderType, 841 ClientTime: time.UnixMilli(int64(cancel.ClientTime)), 842 //ServerTime set in epoch queue processing pipeline. 843 Commit: commit, 844 }, 845 TargetOrderID: targetID, 846 } 847 848 // Send the order to the epoch queue. 849 oRecord := &orderRecord{ 850 order: co, 851 req: cancel, 852 msgID: msg.ID, 853 } 854 if err := tunnel.SubmitOrder(oRecord); err != nil { 855 if errors.Is(err, ErrInternalServer) { 856 log.Errorf("Market failed to SubmitOrder: %v", err) 857 } 858 return msgjson.NewError(msgjson.UnknownMarketError, "%v", err) 859 } 860 return nil 861 } 862 863 // verifyAccount checks that the submitted order squares with the submitting user. 864 func (r *OrderRouter) verifyAccount(user account.AccountID, msgAcct msgjson.Bytes, signable msgjson.Signable) *msgjson.Error { 865 // Verify account ID matches. 866 if !bytes.Equal(user[:], msgAcct) { 867 return msgjson.NewError(msgjson.OrderParameterError, "account ID mismatch") 868 } 869 // Check the clients signature of the order. 870 sigMsg := signable.Serialize() 871 err := r.auth.Auth(user, sigMsg, signable.SigBytes()) 872 if err != nil { 873 return msgjson.NewError(msgjson.SignatureError, "signature error: %v", err.Error()) 874 } 875 return nil 876 } 877 878 // extractMarket finds the MarketTunnel for the provided prefix. 879 func (r *OrderRouter) extractMarket(prefix *msgjson.Prefix) (MarketTunnel, *msgjson.Error) { 880 mktName, err := dex.MarketName(prefix.Base, prefix.Quote) 881 if err != nil { 882 return nil, msgjson.NewError(msgjson.UnknownMarketError, "asset lookup error: %v", err.Error()) 883 } 884 tunnel, found := r.tunnels[mktName] 885 if !found { 886 return nil, msgjson.NewError(msgjson.UnknownMarketError, "unknown market %s", mktName) 887 } 888 return tunnel, nil 889 } 890 891 // SuspendEpoch holds the index and end time of final epoch marking the 892 // suspension of a market. 893 type SuspendEpoch struct { 894 Idx int64 895 End time.Time 896 } 897 898 // SuspendMarket schedules a suspension of a given market, with the option to 899 // persist the orders on the book (or purge the book automatically on market 900 // shutdown). The scheduled final epoch and suspend time are returned. Note that 901 // OrderRouter is a proxy for this request to the ultimate Market. This is done 902 // because OrderRouter is the entry point for new orders into the market. TODO: 903 // track running, suspended, and scheduled-suspended markets, appropriately 904 // blocking order submission according to the schedule rather than just checking 905 // Market.Running prior to submitting incoming orders to the Market. 906 func (r *OrderRouter) SuspendMarket(mktName string, asSoonAs time.Time, persistBooks bool) *SuspendEpoch { 907 mkt, found := r.tunnels[mktName] 908 if !found { 909 return nil 910 } 911 912 idx, t := mkt.Suspend(asSoonAs, persistBooks) 913 return &SuspendEpoch{ 914 Idx: idx, 915 End: t, 916 } 917 } 918 919 // Suspend is like SuspendMarket, but for all known markets. 920 func (r *OrderRouter) Suspend(asSoonAs time.Time, persistBooks bool) map[string]*SuspendEpoch { 921 922 suspendTimes := make(map[string]*SuspendEpoch, len(r.tunnels)) 923 for name, mkt := range r.tunnels { 924 idx, ts := mkt.Suspend(asSoonAs, persistBooks) 925 suspendTimes[name] = &SuspendEpoch{Idx: idx, End: ts} 926 } 927 928 // MarketTunnel.Running will return false when the market closes, and true 929 // when and if it opens again. Locking/blocking of the incoming order 930 // handlers is not necessary since any orders that sneak in to a Market will 931 // be rejected if there is no active epoch. 932 933 return suspendTimes 934 } 935 936 // extractMarketDetails finds the MarketTunnel, an assetSet, and market side for 937 // the provided prefix. 938 func (r *OrderRouter) extractMarketDetails(prefix *msgjson.Prefix, trade *msgjson.Trade) (MarketTunnel, *assetSet, bool, *msgjson.Error) { 939 // Check that assets are for a valid market. 940 tunnel, rpcErr := r.extractMarket(prefix) 941 if rpcErr != nil { 942 return nil, nil, false, rpcErr 943 } 944 // Side must be one of buy or sell 945 var sell bool 946 switch trade.Side { 947 case msgjson.BuyOrderNum: 948 case msgjson.SellOrderNum: 949 sell = true 950 default: 951 return nil, nil, false, msgjson.NewError(msgjson.OrderParameterError, 952 "invalid side value %d", trade.Side) 953 } 954 quote, found := r.assets[prefix.Quote] 955 if !found { 956 panic("missing quote asset for known market should be impossible") 957 } 958 base, found := r.assets[prefix.Base] 959 if !found { 960 panic("missing base asset for known market should be impossible") 961 } 962 return tunnel, newAssetSet(base, quote, sell), sell, nil 963 } 964 965 // checkTimes validates the timestamps in an order prefix. 966 func checkTimes(prefix *msgjson.Prefix) *msgjson.Error { 967 offset := time.Now().UnixMilli() - int64(prefix.ClientTime) 968 if offset < 0 { 969 offset *= -1 970 } 971 if offset >= maxClockOffset { 972 return msgjson.NewError(msgjson.ClockRangeError, 973 "clock offset of %d ms is larger than maximum allowed, %d ms", 974 offset, maxClockOffset, 975 ) 976 } 977 // Server time should be unset. 978 if prefix.ServerTime != 0 { 979 return msgjson.NewError(msgjson.OrderParameterError, "non-zero server time not allowed") 980 } 981 return nil 982 } 983 984 // checkPrefixTrade validates the information in the prefix and trade portions 985 // of an order. 986 func (r *OrderRouter) checkPrefixTrade(assets *assetSet, lotSize uint64, prefix *msgjson.Prefix, 987 trade *msgjson.Trade, checkLot bool) *msgjson.Error { 988 // Check that the client's timestamp is still valid. 989 rpcErr := checkTimes(prefix) 990 if rpcErr != nil { 991 return rpcErr 992 } 993 // Check that the address is valid. 994 if !assets.receiving.Backend.CheckSwapAddress(trade.Address) { 995 return msgjson.NewError(msgjson.OrderParameterError, "address doesn't check") 996 } 997 // Quantity cannot be zero, and must be an integral multiple of the lot size. 998 if trade.Quantity == 0 { 999 return msgjson.NewError(msgjson.OrderParameterError, "zero quantity not allowed") 1000 } 1001 if checkLot && trade.Quantity%lotSize != 0 { 1002 return msgjson.NewError(msgjson.OrderParameterError, "order quantity not a multiple of lot size") 1003 } 1004 // Validate UTXOs 1005 // Check that all required arrays are of equal length. 1006 if len(trade.Coins) == 0 { 1007 return msgjson.NewError(msgjson.FundingError, "order must specify utxos") 1008 } 1009 1010 for i, coin := range trade.Coins { 1011 sigCount := len(coin.Sigs) 1012 if sigCount == 0 { 1013 return msgjson.NewError(msgjson.SignatureError, "no signature for coin %d", i) 1014 } 1015 if len(coin.PubKeys) != sigCount { 1016 return msgjson.NewError(msgjson.OrderParameterError, 1017 "pubkey count %d not equal to signature count %d for coin %d", 1018 len(coin.PubKeys), sigCount, i, 1019 ) 1020 } 1021 } 1022 1023 return nil 1024 } 1025 1026 // msgBytesToBytes converts a []msgjson.Byte to a [][]byte. 1027 func msgBytesToBytes(msgBs []msgjson.Bytes) [][]byte { 1028 b := make([][]byte, 0, len(msgBs)) 1029 for _, msgB := range msgBs { 1030 b = append(b, msgB) 1031 } 1032 return b 1033 } 1034 1035 // fmtCoinID formats the coin ID by asset. If an error is encountered, the 1036 // coinID string returned hex-encoded and prepended with "unparsed:". 1037 func fmtCoinID(assetID uint32, coinID []byte) string { 1038 strID, err := asset.DecodeCoinID(assetID, coinID) 1039 if err != nil { 1040 return "unparsed:" + hex.EncodeToString(coinID) 1041 } 1042 return strID 1043 } 1044 1045 // fmtCoinIDs is like fmtCoinID but for a slice of CoinIDs, printing with 1046 // default Go slice formatting like "[coin1 coin2 ...]". 1047 func fmtCoinIDs(assetID uint32, coinIDs []order.CoinID) string { 1048 out := make([]string, len(coinIDs)) 1049 for i := range coinIDs { 1050 out[i] = fmtCoinID(assetID, coinIDs[i]) 1051 } 1052 return fmt.Sprint(out) 1053 } 1054 1055 func safeMidGap(tunnel MarketTunnel) uint64 { 1056 midGap := tunnel.MidGap() 1057 if midGap == 0 { 1058 return tunnel.RateStep() 1059 } 1060 return midGap 1061 }