decred.org/dcrdex@v1.0.5/server/swap/swap.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 swap 5 6 import ( 7 "bytes" 8 "context" 9 "errors" 10 "fmt" 11 "sync" 12 "sync/atomic" 13 "time" 14 15 "decred.org/dcrdex/dex" 16 "decred.org/dcrdex/dex/calc" 17 "decred.org/dcrdex/dex/encode" 18 "decred.org/dcrdex/dex/meter" 19 "decred.org/dcrdex/dex/msgjson" 20 "decred.org/dcrdex/dex/order" 21 "decred.org/dcrdex/dex/wait" 22 "decred.org/dcrdex/server/account" 23 "decred.org/dcrdex/server/asset" 24 "decred.org/dcrdex/server/auth" 25 "decred.org/dcrdex/server/coinlock" 26 "decred.org/dcrdex/server/comms" 27 "decred.org/dcrdex/server/db" 28 "decred.org/dcrdex/server/matcher" 29 ) 30 31 var ( 32 // The coin waiter will initially query for transaction data every 33 // fastRecheckInterval, but will eventually taper to taperedRecheckInterval. 34 fastRecheckInterval = time.Second * 3 35 taperedRecheckInterval = time.Second * 30 36 // minBlockPeriod is the minimum delay between block-triggered 37 // confirmation/inaction checks. This helps with limiting notification 38 // bursts when blocks are generated closely together (e.g. in Ethereum 39 // occasionally several blocks are generated in a single second). 40 minBlockPeriod = time.Second * 10 41 ) 42 43 func unixMsNow() time.Time { 44 return time.Now().Truncate(time.Millisecond).UTC() 45 } 46 47 func makerTaker(isMaker bool) string { 48 if isMaker { 49 return "maker" 50 } 51 return "taker" 52 } 53 54 // AuthManager handles client-related actions, including authorization and 55 // communications. 56 type AuthManager interface { 57 Route(string, func(account.AccountID, *msgjson.Message) *msgjson.Error) 58 Auth(user account.AccountID, msg, sig []byte) error 59 Sign(...msgjson.Signable) 60 Send(account.AccountID, *msgjson.Message) error 61 Request(account.AccountID, *msgjson.Message, func(comms.Link, *msgjson.Message)) error 62 RequestWithTimeout(user account.AccountID, req *msgjson.Message, handlerFunc func(comms.Link, *msgjson.Message), 63 expireTimeout time.Duration, expireFunc func()) error 64 SwapSuccess(user account.AccountID, mmid db.MarketMatchID, value uint64, refTime time.Time) 65 Inaction(user account.AccountID, misstep auth.NoActionStep, mmid db.MarketMatchID, matchValue uint64, refTime time.Time, oid order.OrderID) 66 } 67 68 // Storage updates match data in what is presumably a database. 69 type Storage interface { 70 db.SwapArchiver 71 LastErr() error 72 Fatal() <-chan struct{} 73 Order(oid order.OrderID, base, quote uint32) (order.Order, order.OrderStatus, error) 74 CancelOrder(*order.LimitOrder) error 75 InsertMatch(match *order.Match) error 76 } 77 78 // swapStatus is information related to the completion or incompletion of each 79 // sequential step of the atomic swap negotiation process. Each user has their 80 // own swapStatus. 81 type swapStatus struct { 82 // The asset to which the user broadcasts their swap transaction. 83 swapAsset uint32 84 redeemAsset uint32 85 86 swapSearching uint32 // atomic 87 redeemSearching uint32 // atomic 88 89 mtx sync.RWMutex 90 // The time that the swap coordinator sees the transaction. 91 swapTime time.Time 92 swap *asset.Contract 93 // The time that the transaction receives its SwapConf'th confirmation. 94 swapConfirmed time.Time 95 // The time that the swap coordinator sees the user's redemption 96 // transaction. 97 redeemTime time.Time 98 redemption asset.Coin 99 } 100 101 // String satisfies the Stringer interface for pretty printing. The swapStatus 102 // RWMutex should be held for reads when using. 103 func (ss *swapStatus) String() string { 104 return fmt.Sprintf("swapAsset: %d, redeemAsset: %d, swapTime: %v, swap: %v, swapConfirmed: %v, redeemTime: %v, redemption: %v", 105 ss.swapAsset, ss.redeemAsset, ss.swapTime, ss.swap, ss.swapConfirmed, ss.redeemTime, ss.redemption) 106 } 107 108 func (ss *swapStatus) startSwapSearch() bool { 109 return atomic.CompareAndSwapUint32(&ss.swapSearching, 0, 1) 110 } 111 112 func (ss *swapStatus) endSwapSearch() { 113 atomic.StoreUint32(&ss.swapSearching, 0) 114 } 115 116 func (ss *swapStatus) startRedeemSearch() bool { 117 return atomic.CompareAndSwapUint32(&ss.redeemSearching, 0, 1) 118 } 119 120 func (ss *swapStatus) endRedeemSearch() { 121 atomic.StoreUint32(&ss.redeemSearching, 0) 122 } 123 124 func (ss *swapStatus) swapConfTime() time.Time { 125 ss.mtx.RLock() 126 defer ss.mtx.RUnlock() 127 return ss.swapConfirmed 128 } 129 130 func (ss *swapStatus) contractState() (known, confirmed bool) { 131 ss.mtx.RLock() 132 defer ss.mtx.RUnlock() 133 return ss.swap != nil, !ss.swapConfirmed.IsZero() 134 } 135 136 func (ss *swapStatus) redeemSeenTime() time.Time { 137 ss.mtx.RLock() 138 defer ss.mtx.RUnlock() 139 return ss.redeemTime 140 } 141 142 // matchTracker embeds an order.Match and adds some data necessary for tracking 143 // the match negotiation. 144 type matchTracker struct { 145 mtx sync.RWMutex // Match.Sigs and Match.Status 146 *order.Match 147 time time.Time // the match request time, not epoch close 148 matchTime time.Time // epoch close time 149 makerStatus *swapStatus 150 takerStatus *swapStatus 151 } 152 153 // expiredBy returns true if the lock time of either party's *known* swap is 154 // before the reference time e.g. time.Now(). 155 func (mt *matchTracker) expiredBy(ref time.Time) bool { 156 mSwap, tSwap := mt.makerStatus.swap, mt.takerStatus.swap 157 return (tSwap != nil && tSwap.LockTime.Before(ref)) || 158 (mSwap != nil && mSwap.LockTime.Before(ref)) 159 } 160 161 // A blockNotification is used internally when an asset.Backend reports a new 162 // block. 163 type blockNotification struct { 164 time time.Time 165 assetID uint32 166 err error 167 } 168 169 // A stepActor is a structure holding information about one party of a match. 170 // stepActor is used with the stepInformation structure, which is used for 171 // sequencing swap negotiation. 172 type stepActor struct { 173 user account.AccountID 174 // swapAsset is the asset to which this actor broadcasts their swap tx. 175 swapAsset uint32 176 isMaker bool 177 order order.Order 178 // The swapStatus from the Match. Could be either the 179 // (matchTracker).makerStatus or (matchTracker).takerStatus, depending on who 180 // this actor is. 181 status *swapStatus 182 } 183 184 // String satisfies the Stringer interface for pretty printing. The swapStatus 185 // RWMutex should be held for reads when using for a.status reads. 186 func (a stepActor) String() string { 187 return fmt.Sprintf("user: %v, swapAsset: %v, isMaker: %v, order: %v, status: {%v}", 188 a.user, a.swapAsset, a.isMaker, a.order, a.status) 189 } 190 191 // stepInformation holds information about the current state of the swap 192 // negotiation. A new stepInformation should be generated with (Swapper).step at 193 // every step of the negotiation process. 194 type stepInformation struct { 195 match *matchTracker 196 // The actor is the user info for the user who is expected to be broadcasting 197 // a swap or redemption transaction next. 198 actor stepActor 199 // counterParty is the user that is not expected to be acting next. 200 counterParty stepActor 201 // asset is the asset backend for swapAsset. 202 asset *asset.BackedAsset 203 // isBaseAsset will be true if the current step involves a transaction on the 204 // match market's base asset blockchain, false if on quote asset's blockchain. 205 isBaseAsset bool 206 step order.MatchStatus 207 nextStep order.MatchStatus 208 // checkVal holds the trade amount in units of the currently acting asset, 209 // and is used to validate the swap transaction details. 210 checkVal uint64 211 } 212 213 // SwapperAsset is a BackedAsset with an optional CoinLocker. 214 type SwapperAsset struct { 215 *asset.BackedAsset 216 Locker coinlock.CoinLocker // should be *coinlock.AssetCoinLocker 217 } 218 219 // Swapper handles order matches by handling authentication and inter-party 220 // communications between clients, or 'users'. The Swapper authenticates users 221 // (vua AuthManager) and validates transactions as they are reported. 222 type Swapper struct { 223 // coins is a map to all the Asset information, including the asset backends, 224 // used by this Swapper. 225 coins map[uint32]*SwapperAsset 226 // storage is a Database backend. 227 storage Storage 228 // authMgr is an AuthManager for client messaging and authentication. 229 authMgr AuthManager 230 // swapDone is callback for reporting a swap outcome. 231 swapDone func(oid order.Order, match *order.Match, fail bool) 232 233 // The matches maps and the contained matches are protected by the matchMtx. 234 matchMtx sync.RWMutex 235 matches map[order.MatchID]*matchTracker 236 userMatches map[account.AccountID]map[order.MatchID]*matchTracker 237 acctMatches map[uint32]map[string]map[order.MatchID]*matchTracker 238 239 // The broadcast timeout. 240 bTimeout time.Duration 241 // txWaitExpiration is the longest the Swapper will wait for a coin waiter. 242 txWaitExpiration time.Duration 243 // Expected locktimes for maker and taker swaps. 244 lockTimeTaker time.Duration 245 lockTimeMaker time.Duration 246 // latencyQ is a queue for coin waiters to deal with network latency. 247 latencyQ *wait.TaperingTickerQueue 248 249 // handlerMtx should be read-locked for the duration of the comms route 250 // handlers (handleInit and handleRedeem) and Negotiate. This blocks 251 // shutdown until any coin waiters are registered with latencyQ. It should 252 // be write-locked before setting the stop flag. 253 handlerMtx sync.RWMutex 254 // stop is used to prevent new handlers from starting coin waiters. It is 255 // set to true during shutdown of Run. 256 stop bool 257 } 258 259 // Config is the swapper configuration settings. A Config instance is the only 260 // argument to the Swapper constructor. 261 type Config struct { 262 // Assets is a map to all the asset information, including the asset backends, 263 // used by this Swapper. 264 Assets map[uint32]*SwapperAsset 265 // AuthManager is the auth manager for client messaging and authentication. 266 AuthManager AuthManager 267 // A database backend. 268 Storage Storage 269 // BroadcastTimeout is how long the Swapper will wait for expected swap 270 // transactions following new blocks. 271 BroadcastTimeout time.Duration 272 // TxWaitExpiration is the longest the Swapper will wait for a coin waiter. 273 // This could be thought of as the maximum allowable backend latency. 274 TxWaitExpiration time.Duration 275 // LockTimeTaker is the locktime Swapper will use for auditing taker swaps. 276 LockTimeTaker time.Duration 277 // LockTimeMaker is the locktime Swapper will use for auditing maker swaps. 278 LockTimeMaker time.Duration 279 // NoResume indicates that the swapper should not resume active swaps. 280 NoResume bool 281 // AllowPartialRestore indicates if it is acceptable to load only some of 282 // the active swaps if the Swapper's asset configuration lacks assets 283 // required to load them all. 284 AllowPartialRestore bool 285 // SwapDone registers a match with the DEX manager (or other consumer) for a 286 // given order as being finished. 287 SwapDone func(oid order.Order, match *order.Match, fail bool) 288 } 289 290 // NewSwapper is a constructor for a Swapper. 291 func NewSwapper(cfg *Config) (*Swapper, error) { 292 for _, asset := range cfg.Assets { 293 if asset.MaxFeeRate == 0 { 294 return nil, fmt.Errorf("max fee rate of 0 is invalid for asset %q", asset.Symbol) 295 } 296 } 297 298 acctMatches := make(map[uint32]map[string]map[order.MatchID]*matchTracker) 299 for _, a := range cfg.Assets { 300 if _, ok := a.Backend.(asset.AccountBalancer); ok { 301 acctMatches[a.ID] = make(map[string]map[order.MatchID]*matchTracker) 302 } 303 } 304 305 authMgr := cfg.AuthManager 306 swapper := &Swapper{ 307 coins: cfg.Assets, 308 storage: cfg.Storage, 309 authMgr: authMgr, 310 swapDone: cfg.SwapDone, 311 latencyQ: wait.NewTaperingTickerQueue(fastRecheckInterval, taperedRecheckInterval), 312 matches: make(map[order.MatchID]*matchTracker), 313 userMatches: make(map[account.AccountID]map[order.MatchID]*matchTracker), 314 acctMatches: acctMatches, 315 bTimeout: cfg.BroadcastTimeout, 316 txWaitExpiration: cfg.TxWaitExpiration, 317 lockTimeTaker: cfg.LockTimeTaker, 318 lockTimeMaker: cfg.LockTimeMaker, 319 } 320 321 // Ensure txWaitExpiration is not greater than broadcast timeout setting. 322 if swapper.txWaitExpiration > swapper.bTimeout { 323 swapper.txWaitExpiration = swapper.bTimeout 324 } 325 326 if !cfg.NoResume { 327 err := swapper.restoreActiveSwaps(cfg.AllowPartialRestore) 328 if err != nil { 329 return nil, err 330 } 331 } 332 333 // The swapper is only concerned with two types of client-originating 334 // method requests. 335 authMgr.Route(msgjson.InitRoute, swapper.handleInit) 336 authMgr.Route(msgjson.RedeemRoute, swapper.handleRedeem) 337 338 return swapper, nil 339 } 340 341 // addMatch registers a match. The matchMtx must be locked. 342 func (s *Swapper) addMatch(mt *matchTracker) { 343 mid := mt.ID() 344 s.matches[mid] = mt 345 346 // Add the match to both maker's and taker's match maps. 347 maker, taker := mt.Maker.User(), mt.Taker.User() 348 for _, user := range []account.AccountID{maker, taker} { 349 userMatches, found := s.userMatches[user] 350 if !found { 351 s.userMatches[user] = map[order.MatchID]*matchTracker{ 352 mid: mt, 353 } 354 } else { 355 userMatches[mid] = mt // may overwrite for self-match (ok) 356 } 357 if maker == taker { 358 break 359 } 360 } 361 362 addAcctMatch := func(matches map[string]map[order.MatchID]*matchTracker, acctAddr string, mt *matchTracker) { 363 acctMatches := matches[acctAddr] 364 if acctMatches == nil { 365 acctMatches = make(map[order.MatchID]*matchTracker, 1) 366 matches[acctAddr] = acctMatches 367 } 368 acctMatches[mt.ID()] = mt 369 } 370 371 if s.acctMatches[mt.Maker.Base()] != nil { 372 acctMatches := s.acctMatches[mt.Maker.Base()] 373 addAcctMatch(acctMatches, mt.Maker.BaseAccount(), mt) 374 addAcctMatch(acctMatches, mt.Taker.Trade().BaseAccount(), mt) 375 } 376 if s.acctMatches[mt.Maker.Quote()] != nil { 377 acctMatches := s.acctMatches[mt.Maker.Quote()] 378 addAcctMatch(acctMatches, mt.Maker.QuoteAccount(), mt) 379 addAcctMatch(acctMatches, mt.Taker.Trade().QuoteAccount(), mt) 380 } 381 } 382 383 // deleteMatch unregisters a match. The matchMtx must be locked. 384 func (s *Swapper) deleteMatch(mt *matchTracker) { 385 mid := mt.ID() 386 delete(s.matches, mid) 387 388 // Unlock the maker and taker order coins. May be redundant if processBlock 389 // confirmed both swaps, but premature/quick counterparty actions that 390 // advance match status first prevent that. 391 s.unlockOrderCoins(mt.Maker) 392 s.unlockOrderCoins(mt.Taker) 393 394 // Remove the match from both maker's and taker's match maps. 395 maker, taker := mt.Maker.User(), mt.Taker.User() 396 for _, user := range []account.AccountID{maker, taker} { 397 userMatches, found := s.userMatches[user] 398 if !found { 399 // Should not happen if consistently using addMatch. 400 log.Errorf("deleteMatch: No matches for user %v found!", user) 401 continue 402 } 403 delete(userMatches, mid) 404 if len(userMatches) == 0 { 405 delete(s.userMatches, user) 406 } 407 if maker == taker { 408 break 409 } 410 } 411 412 deleteAcctMatch := func(matches map[string]map[order.MatchID]*matchTracker, acctAddr string, mt *matchTracker) { 413 acctMatches := matches[acctAddr] 414 if acctMatches == nil { 415 return 416 } 417 delete(acctMatches, mt.ID()) 418 if len(acctMatches) == 0 { 419 delete(matches, acctAddr) 420 } 421 } 422 423 if s.acctMatches[mt.Maker.Base()] != nil { 424 acctMatches := s.acctMatches[mt.Maker.Base()] 425 deleteAcctMatch(acctMatches, mt.Maker.BaseAccount(), mt) 426 deleteAcctMatch(acctMatches, mt.Taker.Trade().BaseAccount(), mt) 427 } 428 if s.acctMatches[mt.Maker.Quote()] != nil { 429 acctMatches := s.acctMatches[mt.Maker.Quote()] 430 deleteAcctMatch(acctMatches, mt.Maker.QuoteAccount(), mt) 431 deleteAcctMatch(acctMatches, mt.Taker.Trade().QuoteAccount(), mt) 432 } 433 } 434 435 // UnsettledQuantity sums up the settling quantity per market for a user. Part 436 // of the market.MatchSwapper interface. 437 func (s *Swapper) UnsettledQuantity(user account.AccountID) map[[2]uint32]uint64 { 438 s.matchMtx.RLock() 439 defer s.matchMtx.RUnlock() 440 marketQuantities := make(map[[2]uint32]uint64) 441 userMatches, found := s.userMatches[user] 442 if !found { 443 return marketQuantities 444 } 445 for _, mt := range userMatches { 446 mt.mtx.RLock() 447 matchStatus := mt.Status 448 mt.mtx.RUnlock() 449 if mt.Maker.AccountID == user { 450 if matchStatus >= order.MakerRedeemed { 451 continue 452 } 453 } else if matchStatus >= order.TakerSwapCast { 454 continue 455 } 456 mktID := [2]uint32{mt.Maker.BaseAsset, mt.Maker.QuoteAsset} 457 marketQuantities[mktID] += mt.Quantity 458 } 459 return marketQuantities 460 } 461 462 // pendingAccountStats is used to sum in-process match stats for the 463 // AccountStats method. 464 type pendingAccountStats struct { 465 acctAddr string 466 assetID uint32 467 swaps uint64 468 qty uint64 469 redeems int 470 } 471 472 func newPendingAccountStats(acctAddr string, assetID uint32) *pendingAccountStats { 473 return &pendingAccountStats{ 474 acctAddr: acctAddr, 475 assetID: assetID, 476 } 477 } 478 479 func (p *pendingAccountStats) addMatch(mt *matchTracker) { 480 p.addOrder(mt, mt.Maker, order.MakerSwapCast, order.MakerRedeemed) 481 p.addOrder(mt, mt.Taker, order.TakerSwapCast, order.MatchComplete) 482 } 483 484 func (p *pendingAccountStats) addOrder(mt *matchTracker, ord order.Order, swappedStatus, redeemedStatus order.MatchStatus) { 485 trade := ord.Trade() 486 if ord.Base() == p.assetID && trade.BaseAccount() == p.acctAddr { 487 if trade.Sell { 488 if mt.Status < swappedStatus { 489 p.qty += mt.Quantity 490 p.swaps++ 491 } 492 } else if mt.Status < redeemedStatus { 493 p.redeems++ 494 } 495 } 496 if ord.Quote() == p.assetID && trade.QuoteAccount() == p.acctAddr { 497 if !trade.Sell { 498 if mt.Status < swappedStatus { 499 p.qty += calc.BaseToQuote(mt.Rate, mt.Quantity) 500 p.swaps++ // The swap is expected to occur in 1 transaction. 501 } 502 } else if mt.Status < redeemedStatus { 503 p.redeems++ 504 } 505 } 506 } 507 508 // AccountStats is part of the MatchNegotiator interface to report in-process 509 // match information for a asset account address. 510 func (s *Swapper) AccountStats(acctAddr string, assetID uint32) (qty, swaps uint64, redeems int) { 511 stats := newPendingAccountStats(acctAddr, assetID) 512 s.matchMtx.RLock() 513 defer s.matchMtx.RUnlock() 514 acctMatches := s.acctMatches[assetID] 515 if acctMatches == nil { 516 return // How? 517 } 518 for _, mt := range acctMatches[acctAddr] { 519 stats.addMatch(mt) 520 } 521 return stats.qty, stats.swaps, stats.redeems 522 } 523 524 // ChainsSynced will return true if both specified asset's backends are synced. 525 func (s *Swapper) ChainsSynced(base, quote uint32) (bool, error) { 526 b, found := s.coins[base] 527 if !found { 528 return false, fmt.Errorf("no backend found for %d", base) 529 } 530 baseSynced, err := b.Backend.Synced() 531 if err != nil { 532 return false, fmt.Errorf("error checking sync status for %d: %w", base, err) 533 } 534 if !baseSynced { 535 return false, nil 536 } 537 q, found := s.coins[quote] 538 if !found { 539 return false, fmt.Errorf("no backend found for %d", base) 540 } 541 quoteSynced, err := q.Backend.Synced() 542 if err != nil { 543 return false, fmt.Errorf("error checking sync status for %d: %w", quote, err) 544 } 545 return quoteSynced, nil 546 } 547 548 func (s *Swapper) restoreActiveSwaps(allowPartial bool) error { 549 // Load active swap data from DB. 550 swapData, err := s.storage.ActiveSwaps() 551 if err != nil { 552 return err 553 } 554 log.Infof("Loaded swap data for %d active swaps.", len(swapData)) 555 if len(swapData) == 0 { 556 return nil 557 } 558 559 // Check that the required assets backends are available. 560 missingAssets := make(map[uint32]bool) 561 checkAsset := func(id uint32) { 562 if s.coins[id] == nil && !missingAssets[id] { 563 log.Warnf("Unable to find backend for asset %d with active swaps.", id) 564 missingAssets[id] = true 565 } 566 } 567 for _, sd := range swapData { 568 checkAsset(sd.Base) 569 checkAsset(sd.Quote) 570 } 571 572 if len(missingAssets) > 0 && !allowPartial { 573 return fmt.Errorf("missing backend for asset with active swaps") 574 } 575 576 // Load the matchTrackers, calling the Contract and Redemption asset.Backend 577 // methods as needed. 578 579 type swapStatusData struct { 580 SwapAsset uint32 // from market schema and takerSell bool 581 RedeemAsset uint32 582 SwapTime int64 // {a,b}ContractTime 583 ContractCoinOut []byte // {a,b}ContractCoinID 584 ContractScript []byte // {a,b}Contract 585 RedeemTime int64 // {a,b}RedeemTime 586 RedeemCoinIn []byte // {a,b}aRedeemCoinID 587 // SwapConfirmTime is not stored in the DB, so use time.Now() if the 588 // contract has reached SwapConf. 589 } 590 591 translateSwapStatus := func(ss *swapStatus, ssd *swapStatusData, cpSwapCoin []byte) error { 592 ss.swapAsset, ss.redeemAsset = ssd.SwapAsset, ssd.RedeemAsset 593 594 swapCoin := ssd.ContractCoinOut 595 if len(swapCoin) > 0 { 596 assetID := ssd.SwapAsset 597 swapAsset := s.coins[assetID] 598 swap, err := swapAsset.Backend.Contract(swapCoin, ssd.ContractScript) 599 if err != nil { 600 return fmt.Errorf("unable to find swap out coin %x for asset %d: %w", swapCoin, assetID, err) 601 } 602 ss.swap = swap 603 ss.swapTime = time.UnixMilli(ssd.SwapTime) 604 605 swapConfs, err := swap.Confirmations(context.Background()) 606 if err != nil { 607 log.Warnf("No swap confirmed time for %v: %v", swap, err) 608 } else if swapConfs >= int64(swapAsset.SwapConf) { 609 // We don't record the time at which we saw the block that got 610 // the swap to SwapConf, so give the user extra time. 611 ss.swapConfirmed = time.Now().UTC() 612 } 613 } 614 615 if redeemCoin := ssd.RedeemCoinIn; len(redeemCoin) > 0 { 616 assetID := ssd.RedeemAsset 617 redeem, err := s.coins[assetID].Backend.Redemption(redeemCoin, cpSwapCoin, ssd.ContractScript) 618 if err != nil { 619 return fmt.Errorf("unable to find redeem in coin %x for asset %d: %w", redeemCoin, assetID, err) 620 } 621 ss.redemption = redeem 622 ss.redeemTime = time.UnixMilli(ssd.RedeemTime) 623 } 624 625 return nil 626 } 627 628 s.matches = make(map[order.MatchID]*matchTracker, len(swapData)) 629 s.userMatches = make(map[account.AccountID]map[order.MatchID]*matchTracker) 630 for _, sd := range swapData { 631 if missingAssets[sd.Base] { 632 log.Warnf("Dropping match %v with no backend available for base asset %d", sd.ID, sd.Base) 633 continue 634 } 635 if missingAssets[sd.Quote] { 636 log.Warnf("Dropping match %v with no backend available for quote asset %d", sd.ID, sd.Quote) 637 continue 638 } 639 // Load the maker's order.LimitOrder and taker's order.Order. WARNING: 640 // This is a different Order instance from whatever Market or other 641 // subsystems might have. As such, the mutable fields or accessors of 642 // mutable data should not be used. 643 taker, _, err := s.storage.Order(sd.MatchData.Taker, sd.Base, sd.Quote) 644 if err != nil { 645 log.Errorf("Failed to load taker order: %v", err) 646 continue 647 } 648 if taker.ID() != sd.MatchData.Taker { 649 log.Errorf("Failed to load order %v, computed ID %v instead", sd.MatchData.Taker, taker.ID()) 650 continue 651 } 652 maker, _, err := s.storage.Order(sd.MatchData.Maker, sd.Base, sd.Quote) 653 if err != nil { 654 log.Errorf("Failed to load taker order: %v", err) 655 continue 656 } 657 if maker.ID() != sd.MatchData.Maker { 658 log.Errorf("Failed to load order %v, computed ID %v instead", sd.MatchData.Maker, maker.ID()) 659 continue 660 } 661 makerLO, ok := maker.(*order.LimitOrder) 662 if !ok { 663 log.Errorf("Maker order was not a limit order: %T", maker) 664 continue 665 } 666 667 match := &order.Match{ 668 Taker: taker, 669 Maker: makerLO, 670 Quantity: sd.Quantity, 671 Rate: sd.Rate, 672 FeeRateBase: sd.BaseRate, 673 FeeRateQuote: sd.QuoteRate, 674 Epoch: sd.Epoch, 675 Status: sd.Status, 676 Sigs: order.Signatures{ // not really needed 677 MakerMatch: sd.SwapData.SigMatchAckMaker, 678 TakerMatch: sd.SwapData.SigMatchAckTaker, 679 MakerAudit: sd.SwapData.ContractAAckSig, 680 TakerAudit: sd.SwapData.ContractBAckSig, 681 TakerRedeem: sd.SwapData.RedeemAAckSig, 682 }, 683 } 684 685 mid := sd.MatchData.ID 686 if mid != match.ID() { // serialization is order IDs, qty, and rate 687 log.Errorf("Failed to load Match %v, computed ID %v instead", mid, match.ID()) 688 continue 689 } 690 691 // Check and skip matches for missing assets. 692 makerSwapAsset, makerRedeemAsset := sd.Base, sd.Quote // maker selling -> their swap asset is base 693 if sd.TakerSell { // maker buying -> their swap asset is quote 694 makerSwapAsset, makerRedeemAsset = sd.Quote, sd.Base 695 } 696 if missingAssets[makerSwapAsset] { 697 log.Infof("Skipping match %v with missing asset %d backend", mid, makerSwapAsset) 698 continue 699 } 700 if missingAssets[makerRedeemAsset] { 701 log.Infof("Skipping match %v with missing asset %d backend", mid, makerRedeemAsset) 702 continue 703 } 704 705 epochCloseTime := match.Epoch.End() 706 mt := &matchTracker{ 707 Match: match, 708 time: epochCloseTime.Add(time.Minute), // not quite, just be generous 709 matchTime: epochCloseTime, 710 makerStatus: &swapStatus{}, // populated by translateSwapStatus 711 takerStatus: &swapStatus{}, 712 } 713 714 makerStatus := &swapStatusData{ 715 SwapAsset: makerSwapAsset, 716 RedeemAsset: makerRedeemAsset, 717 SwapTime: sd.SwapData.ContractATime, 718 ContractCoinOut: sd.SwapData.ContractACoinID, 719 ContractScript: sd.SwapData.ContractA, 720 RedeemTime: sd.SwapData.RedeemATime, 721 RedeemCoinIn: sd.SwapData.RedeemACoinID, 722 } 723 takerStatus := &swapStatusData{ 724 SwapAsset: makerRedeemAsset, 725 RedeemAsset: makerSwapAsset, 726 SwapTime: sd.SwapData.ContractBTime, 727 ContractCoinOut: sd.SwapData.ContractBCoinID, 728 ContractScript: sd.SwapData.ContractB, 729 RedeemTime: sd.SwapData.RedeemBTime, 730 RedeemCoinIn: sd.SwapData.RedeemBCoinID, 731 } 732 733 if err := translateSwapStatus(mt.makerStatus, makerStatus, takerStatus.ContractCoinOut); err != nil { 734 log.Errorf("Loading match %v failed: %v", mid, err) 735 continue 736 } 737 if err := translateSwapStatus(mt.takerStatus, takerStatus, makerStatus.ContractCoinOut); err != nil { 738 log.Errorf("Loading match %v failed: %v", mid, err) 739 continue 740 } 741 742 log.Infof("Resuming swap %v in status %v", mid, mt.Status) 743 s.addMatch(mt) 744 } 745 746 // Live coin waiters are abandoned on Swapper shutdown. When a client 747 // reconnects or their init request times out, they will resend it. 748 749 return nil 750 } 751 752 // Run is the main Swapper loop. It's primary purpose is to update transaction 753 // confirmations when new blocks are mined, and to trigger inaction checks. 754 func (s *Swapper) Run(ctx context.Context) { 755 // Permit internal cancel on anomaly such as storage failure. 756 ctxMaster, cancel := context.WithCancel(ctx) 757 758 // Graceful shutdown first allows active incoming messages to be handled, 759 // blocks more incoming messages in the handler functions, stops the helper 760 // goroutines (latency queue used by the handlers, and the block ntfn 761 // receiver), and finally the main loop via the mainLoop channel. 762 var wgHelpers, wgMain sync.WaitGroup 763 ctxHelpers, cancelHelpers := context.WithCancel(context.Background()) 764 mainLoop := make(chan struct{}) // close after helpers stop for graceful shutdown 765 defer func() { 766 // Stop handlers receiving messages and queueing latency Waiters. 767 s.handlerMtx.Lock() // block until active handlers return 768 s.stop = true // prevent new handlers from starting waiters 769 // NOTE: could also do authMgr.Route(msgjson.{InitRoute,RedeemRoute}, shuttingDownHandler) 770 s.handlerMtx.Unlock() 771 772 // Stop the latencyQ of Waiters and the block update goroutines that 773 // send to the main loop. 774 cancelHelpers() 775 wgHelpers.Wait() 776 777 // Now that handlers AND the coin waiter queue are stopped, the 778 // liveWaiters can be accessed without locking. 779 780 // Stop the main loop if there was no internal error. 781 close(mainLoop) 782 wgMain.Wait() 783 }() 784 785 // Start a listen loop for each asset's block channel. Normal shutdown stops 786 // this before the main loop since this sends to the main loop. 787 blockNotes := make(chan *blockNotification, 32*len(s.coins)) 788 addAsset := func(assetID uint32, blockSource <-chan *asset.BlockUpdate) { 789 errOut, errIn := meter.DelayedRelay(ctxHelpers, minBlockPeriod, 32) 790 wgHelpers.Add(1) 791 go func() { 792 defer wgHelpers.Done() 793 for { 794 select { 795 case blk, ok := <-blockSource: 796 if !ok { 797 log.Errorf("Asset %d has closed the block channel.", assetID) 798 return 799 } 800 801 select { 802 case errIn <- blk.Err: 803 default: // if blocking, the relay is either metering anyway or spewing errors 804 } 805 806 case blkErr, ok := <-errOut: // nils are metered and aggregated 807 if !ok { // relay stopped 808 return 809 } 810 select { 811 case <-ctxHelpers.Done(): 812 return 813 case blockNotes <- &blockNotification{ 814 time: time.Now().UTC(), 815 assetID: assetID, 816 err: blkErr, 817 }: 818 } 819 820 case <-ctxHelpers.Done(): 821 return 822 } 823 } 824 }() 825 } 826 for assetID, lockable := range s.coins { 827 addAsset(assetID, lockable.Backend.BlockChannel(32)) 828 } 829 830 // Start the queue of coinwaiters for the init and redeem handlers. The 831 // handlers must be stopped/blocked before stopping this. 832 wgHelpers.Add(1) 833 go func() { 834 s.latencyQ.Run(ctxHelpers) 835 wgHelpers.Done() 836 }() 837 838 log.Debugf("Swapper started with %v broadcast timeout and %v tx wait expiration.", s.bTimeout, s.txWaitExpiration) 839 840 // Block-based inaction checks are started with Timers, and run in the main 841 // loop to avoid locks and WaitGroups. 842 bcastBlockTrigger := make(chan uint32, 32*len(s.coins)) 843 scheduleInactionCheck := func(assetID uint32) { 844 time.AfterFunc(s.bTimeout, func() { 845 // TODO: This pattern would still send the block trigger half of the 846 // time if the ctxMaster is canceled. 847 if ctxMaster.Err() != nil { 848 return 849 } 850 select { 851 case bcastBlockTrigger <- assetID: // all checks run in main loop 852 case <-ctxMaster.Done(): 853 } 854 }) 855 } 856 857 // On startup, schedule an inaction check for each asset. Ideally these 858 // would start bTimeout after the best block times. 859 for assetID := range s.coins { 860 scheduleInactionCheck(assetID) 861 } 862 863 // Event-based action checks are started with a single ticker. Each of the 864 // events, e.g. match request, could start a timer, but this is simpler and 865 // allows batching the match checks. 866 bcastEventTrigger := bufferedTicker(ctxMaster, s.bTimeout/4) 867 868 processBlockWithTimeout := func(block *blockNotification) { 869 ctxTime, cancelTimeCtx := context.WithTimeout(ctxMaster, 5*time.Second) 870 defer cancelTimeCtx() 871 s.processBlock(ctxTime, block) 872 } 873 874 // Main loop can stop on internal error via cancel(), or when the caller 875 // cancels the parent context triggering graceful shutdown. 876 wgMain.Add(1) 877 go func() { 878 defer wgMain.Done() 879 defer cancel() // ctxMaster for anomalous return 880 for { 881 select { 882 case <-s.storage.Fatal(): 883 return 884 case block := <-blockNotes: 885 if block.err != nil { 886 var connectionErr asset.ConnectionError 887 if errors.As(block.err, &connectionErr) { 888 // Connection issues handling can be triggered here. 889 log.Errorf("connection error detected for %d: %v", block.assetID, block.err) 890 } else { 891 log.Errorf("asset %d is reporting a block notification error: %v", block.assetID, block.err) 892 } 893 continue 894 } 895 896 // processBlock will update confirmation times in the swapStatus 897 // structs. 898 processBlockWithTimeout(block) 899 900 // Schedule an inaction check for matches that involve this 901 // asset, as they could be expecting user action within bTimeout 902 // of this event. 903 scheduleInactionCheck(block.assetID) 904 905 case assetID := <-bcastBlockTrigger: 906 // There was a new block for this asset bTimeout ago. 907 s.checkInactionBlockBased(assetID) 908 909 case <-bcastEventTrigger: 910 // Inaction checks that are not relative to blocks. 911 s.checkInactionEventBased() 912 913 case <-mainLoop: 914 return 915 } 916 } 917 }() 918 919 // Wait for caller cancel or anomalous return from main loop. 920 <-ctxMaster.Done() 921 } 922 923 // bufferedTicker creates a "ticker" that periodically sends on the returned 924 // channel, which has a buffer of length 1 and thus suitable for use in a select 925 // with other events that might cause a regular Ticker send to be dropped. 926 func bufferedTicker(ctx context.Context, dur time.Duration) chan struct{} { 927 buffered := make(chan struct{}, 1) // only need 1 since back-to-back is pointless 928 go func() { 929 ticker := time.NewTicker(dur) 930 defer ticker.Stop() 931 for { 932 select { 933 case <-ticker.C: 934 buffered <- struct{}{} 935 case <-ctx.Done(): 936 return 937 } 938 } 939 }() 940 return buffered 941 } 942 943 func (s *Swapper) tryConfirmSwap(ctx context.Context, status *swapStatus, confTime time.Time) (final bool) { 944 if known, confirmed := status.contractState(); !known { 945 return // no swap yet to confirm 946 } else if confirmed { 947 return true // already confirmed 948 } 949 950 // Swap known means status.swap is set, and that it will not be replaced 951 // because we are gating processInit with the swapSearching semaphore. 952 confs, err := status.swap.Confirmations(ctx) 953 if err != nil { 954 log.Warnf("Unable to get confirmations for swap tx %v: %v", status.swap.TxID(), err) 955 return 956 } 957 958 status.mtx.Lock() 959 defer status.mtx.Unlock() 960 if !status.swapConfirmed.IsZero() { // in case a concurrent check already marked it 961 return true 962 } 963 964 swapConf := s.coins[status.swapAsset].SwapConf // swapStatus exists, therefore swapAsset is in the map 965 if confs >= int64(swapConf) { 966 log.Debugf("Swap %v (%s) has reached %d confirmations (%d required)", 967 status.swap, dex.BipIDSymbol(status.swapAsset), confs, swapConf) 968 status.swapConfirmed = confTime.UTC() 969 final = true 970 } 971 return 972 } 973 974 func (s *Swapper) matchSlice() []*matchTracker { 975 s.matchMtx.RLock() 976 defer s.matchMtx.RUnlock() 977 matches := make([]*matchTracker, 0, len(s.matches)) 978 for _, match := range s.matches { 979 matches = append(matches, match) 980 } 981 return matches 982 } 983 984 // processBlock scans the matches and updates a swapConfirmed time if the 985 // required confirmations are reached. Once a relevant transaction has the 986 // requisite number of confirmations, the next-to-act has only duration 987 // (Swapper).bTimeout to broadcast the next transaction in the settlement 988 // sequence. The timeout is not evaluated here, but in (Swapper).checkInaction. 989 // This method simply sets swapConfirmed in the last actor's swapStatus. 990 func (s *Swapper) processBlock(ctx context.Context, block *blockNotification) { 991 for _, match := range s.matchSlice() { 992 // If it's neither of the match assets, nothing to do. 993 if match.makerStatus.swapAsset != block.assetID && 994 match.takerStatus.swapAsset != block.assetID { 995 return 996 } 997 998 // Lock the matchTracker so the following checks and updates are atomic 999 // with respect to Status. 1000 match.mtx.RLock() 1001 defer match.mtx.RUnlock() 1002 1003 switch match.Status { 1004 case order.MakerSwapCast: 1005 if match.makerStatus.swapAsset != block.assetID { 1006 break 1007 } 1008 // If the maker has broadcast their transaction, the taker's 1009 // broadcast timeout starts once the maker's swap has SwapConf 1010 // confs. 1011 if s.tryConfirmSwap(ctx, match.makerStatus, block.time) { 1012 s.unlockOrderCoins(match.Maker) 1013 } 1014 case order.TakerSwapCast: 1015 if match.takerStatus.swapAsset != block.assetID { 1016 break 1017 } 1018 // If the taker has broadcast their transaction, the maker's 1019 // broadcast timeout (for redemption) starts once the taker's swap 1020 // has SwapConf confs. 1021 if s.tryConfirmSwap(ctx, match.takerStatus, block.time) { 1022 s.unlockOrderCoins(match.Taker) 1023 } 1024 } 1025 } 1026 } 1027 1028 // failMatch revokes the match and marks the swap as done for accounting 1029 // purposes. If userFault is false, there will be no penalty, such as if the 1030 // failure is because a swap tx lock time expired before required confirmations 1031 // were reached. 1032 func (s *Swapper) failMatch(match *matchTracker, userFault bool) { 1033 // From the match status, determine maker/taker fault and the corresponding 1034 // auth.NoActionStep. 1035 var makerFault bool 1036 var misstep auth.NoActionStep 1037 var refTime time.Time // a reference time found in the DB for reproducibly sorting outcomes 1038 switch match.Status { 1039 case order.NewlyMatched: 1040 misstep = auth.NoSwapAsMaker 1041 refTime = match.Epoch.End() 1042 makerFault = true 1043 case order.MakerSwapCast: 1044 misstep = auth.NoSwapAsTaker 1045 refTime = match.makerStatus.swapTime // swapConfirmed time is not in the DB 1046 case order.TakerSwapCast: 1047 misstep = auth.NoRedeemAsMaker 1048 refTime = match.takerStatus.swapTime // swapConfirmed time is not in the DB 1049 makerFault = true 1050 case order.MakerRedeemed: 1051 misstep = auth.NoRedeemAsTaker 1052 refTime = match.makerStatus.redeemTime 1053 default: 1054 log.Errorf("Invalid failMatch status %v for match %v", match.Status, match.ID()) 1055 return 1056 } 1057 1058 orderAtFault, otherOrder := match.Taker, order.Order(match.Maker) // an order.Order 1059 if makerFault { 1060 orderAtFault, otherOrder = match.Maker, match.Taker 1061 } 1062 log.Debugf("failMatch: swap %v failing at %v (%v), user fault = %v", 1063 match.ID(), match.Status, misstep, userFault) 1064 1065 // Record the end of this match's processing. 1066 s.storage.SetMatchInactive(db.MatchID(match.Match), !userFault) 1067 1068 // Cancellation rate accounting 1069 s.swapDone(orderAtFault, match.Match, userFault) // will also unbook/revoke order if needed 1070 1071 // Accounting for the maker has already taken place if they have redeemed. 1072 if match.Status != order.MakerRedeemed { 1073 s.swapDone(otherOrder, match.Match, false) 1074 } 1075 1076 // Register the failure to act violation, adjusting the user's score. 1077 if userFault { 1078 s.authMgr.Inaction(orderAtFault.User(), misstep, db.MatchID(match.Match), 1079 match.Quantity, refTime, orderAtFault.ID()) 1080 } 1081 1082 // Send the revoke_match messages, and solicit acks. 1083 s.revoke(match) 1084 } 1085 1086 type fail struct { 1087 match *matchTracker 1088 fault bool 1089 } 1090 1091 // checkInactionEventBased scans the swapStatus structures, checking for actions 1092 // that are expected in a time frame relative to another event that is not a 1093 // confirmation time. If a client is found to have not acted when required, a 1094 // match may be revoked and a penalty assigned to the user. This includes 1095 // matches in NewlyMatched that have not received a maker swap following the 1096 // match request, and in MakerRedeemed that have not received a taker redeem 1097 // following the redemption request triggered by the makers redeem. 1098 func (s *Swapper) checkInactionEventBased() { 1099 // If the DB is failing, do not penalize or attempt to start revocations. 1100 if err := s.storage.LastErr(); err != nil { 1101 log.Errorf("DB in failing state.") 1102 return 1103 } 1104 1105 var failures []fail 1106 1107 // Do time.Since(event) with the same now time for each match. 1108 now := time.Now() 1109 tooOld := func(evt time.Time) bool { 1110 return now.Sub(evt) >= s.bTimeout 1111 } 1112 1113 checkMatch := func(match *matchTracker) { 1114 // Lock entire matchTracker so the following is atomic with respect to 1115 // Status. 1116 match.mtx.RLock() 1117 defer match.mtx.RUnlock() 1118 1119 log.Tracef("checkInactionEventBased: match %v (%v)", match.ID(), match.Status) 1120 1121 deleteMatch := func(fault bool) { 1122 s.deleteMatch(match) 1123 failures = append(failures, fail{match, fault}) // to process after map delete 1124 } 1125 1126 switch match.Status { 1127 case order.NewlyMatched: 1128 // Maker has not broadcast their swap. They have until match time 1129 // plus bTimeout. 1130 if tooOld(match.time) { 1131 deleteMatch(true) 1132 } 1133 case order.MakerSwapCast: 1134 // If the taker contract's expected lock time would be in the past, 1135 // revoke this match with no penalty. 1136 expectedTakerLockTime := match.matchTime.Add(s.lockTimeTaker) 1137 if expectedTakerLockTime.Before(now) { 1138 log.Infof("Revoking match %v at %v because the expected taker swap locktime would be in the past (%v).", 1139 match.ID(), match.Status, expectedTakerLockTime) 1140 deleteMatch(false) 1141 } else if match.expiredBy(now) { 1142 // The taker's contract should expire first, but also check the 1143 // lock time of the maker's known swap. 1144 log.Warnf("Revoking match %v at %v because maker's published contract has expired.", 1145 match.ID(), match.Status) // WRN because taker's should expire first 1146 deleteMatch(false) 1147 } 1148 case order.TakerSwapCast: 1149 // If either published contract's lock time is already passed, 1150 // revoke with no penalty because the swap cannot complete safely. 1151 if match.expiredBy(now) { 1152 log.Infof("Revoking match %v at %v because at least one published contract has expired.", 1153 match.ID(), match.Status) 1154 deleteMatch(false) 1155 } 1156 case order.MakerRedeemed: 1157 // If the maker has redeemed, the taker can redeem immediately, so 1158 // check the timeout against the time the Swapper received the 1159 // maker's `redeem` request (and sent the taker's 'redemption'). 1160 if tooOld(match.makerStatus.redeemSeenTime()) { // rlocks swapStatus.mtx 1161 deleteMatch(true) 1162 } 1163 case order.MatchComplete: 1164 // If we got an ack from the redemption request sent to maker 1165 // (detailing the taker's redeem), or it has been a while since 1166 // taker redeemed, delete the match. Former should have deleted it. 1167 if len(match.Sigs.MakerRedeem) > 0 || tooOld(match.takerStatus.redeemSeenTime()) { 1168 log.Debugf("Deleting completed match %v", match.ID()) 1169 s.deleteMatch(match) // no fail or revoke, just remove from map 1170 } 1171 } 1172 } 1173 1174 // Check and delete atomically 1175 s.matchMtx.Lock() 1176 for _, match := range s.matches { 1177 checkMatch(match) 1178 } 1179 s.matchMtx.Unlock() 1180 1181 // Record failed matches in the DB and auth mgr, unlock coins, and send 1182 // revoke_match messages. 1183 for _, fail := range failures { 1184 s.failMatch(fail.match, fail.fault) 1185 } 1186 } 1187 1188 // checkInactionBlockBased scans the swapStatus structures relevant to the 1189 // specified asset. If a client is found to have not acted when required, a 1190 // match may be revoked and a penalty assigned to the user. This includes 1191 // matches in MakerSwapCast that have not received a taker swap after the 1192 // maker's swap reaches the required confirmation count, and in TakerSwapCast 1193 // that have not received a maker redeem after the taker's swap reaches the 1194 // required confirmation count. 1195 func (s *Swapper) checkInactionBlockBased(assetID uint32) { 1196 // If the DB is failing, do not penalize or attempt to start revocations. 1197 if err := s.storage.LastErr(); err != nil { 1198 log.Errorf("DB in failing state.") 1199 return 1200 } 1201 1202 var failures []fail 1203 // Do time.Since(event) with the same now time for each match. 1204 now := time.Now() 1205 tooOld := func(evt time.Time) bool { 1206 // If the time is not set (zero), it has not happened yet (not too old). 1207 return !evt.IsZero() && now.Sub(evt) >= s.bTimeout 1208 } 1209 1210 checkMatch := func(match *matchTracker) { 1211 if match.makerStatus.swapAsset != assetID && match.takerStatus.swapAsset != assetID { 1212 return 1213 } 1214 1215 // Lock entire matchTracker so the following is atomic with respect to 1216 // Status. 1217 match.mtx.RLock() 1218 defer match.mtx.RUnlock() 1219 1220 log.Tracef("checkInactionBlockBased: asset %d, match %v (%v)", 1221 assetID, match.ID(), match.Status) 1222 1223 deleteMatch := func() { 1224 // Fail the match, and assign fault if lock times are not passed. 1225 s.deleteMatch(match) 1226 failures = append(failures, fail{match, !match.expiredBy(now)}) 1227 } 1228 1229 switch match.Status { 1230 case order.MakerSwapCast: 1231 if tooOld(match.makerStatus.swapConfTime()) { // rlocks swapStatus.mtx 1232 deleteMatch() 1233 } 1234 case order.TakerSwapCast: 1235 if tooOld(match.takerStatus.swapConfTime()) { 1236 deleteMatch() 1237 } 1238 } 1239 } 1240 1241 // Check and delete atomically. 1242 s.matchMtx.Lock() 1243 for _, match := range s.matches { 1244 checkMatch(match) 1245 } 1246 s.matchMtx.Unlock() 1247 1248 // Record failed matches in the DB and auth mgr, unlock coins, and send 1249 // revoke_match messages. 1250 for _, fail := range failures { 1251 s.failMatch(fail.match, fail.fault) 1252 } 1253 } 1254 1255 // respondError sends an rpcError to a user. 1256 func (s *Swapper) respondError(id uint64, user account.AccountID, code int, errMsg string) { 1257 log.Debugf("Error going to user %v, code: %d, msg: %s", user, code, errMsg) 1258 msg, err := msgjson.NewResponse(id, nil, &msgjson.Error{ 1259 Code: code, 1260 Message: errMsg, 1261 }) 1262 if err != nil { 1263 log.Errorf("Failed to create error response with message '%s': %v", msg, err) 1264 return // this should not be possible, but don't pass nil msg to Send 1265 } 1266 if err := s.authMgr.Send(user, msg); err != nil { 1267 log.Infof("Unable to send error response (code = %d, msg = %s) to disconnected user %v: %q", 1268 code, errMsg, user, err) 1269 } 1270 } 1271 1272 // respondSuccess sends a successful response to a user. 1273 func (s *Swapper) respondSuccess(id uint64, user account.AccountID, result any) { 1274 msg, err := msgjson.NewResponse(id, result, nil) 1275 if err != nil { 1276 log.Errorf("failed to send success: %v", err) 1277 return // this should not be possible, but don't pass nil msg to Send 1278 } 1279 if err := s.authMgr.Send(user, msg); err != nil { 1280 log.Infof("Unable to send success response to disconnected user %v: %v", user, err) 1281 } 1282 } 1283 1284 // step creates a stepInformation structure for the specified match. A new 1285 // stepInformation should be created for every client communication. The user 1286 // is also validated as the actor. An error is returned if the user has not 1287 // acknowledged their previous DEX requests. 1288 func (s *Swapper) step(user account.AccountID, matchID order.MatchID) (*stepInformation, *msgjson.Error) { 1289 s.matchMtx.RLock() 1290 match, found := s.matches[matchID] 1291 s.matchMtx.RUnlock() 1292 if !found { 1293 return nil, &msgjson.Error{ 1294 Code: msgjson.RPCUnknownMatch, 1295 Message: "unknown match ID", 1296 } 1297 } 1298 1299 // Get the step-related information for both parties. 1300 var isBaseAsset bool 1301 var actor, counterParty stepActor 1302 var nextStep order.MatchStatus 1303 maker, taker := match.Maker, match.Taker 1304 1305 // Lock for Status and Sigs. 1306 match.mtx.RLock() 1307 defer match.mtx.RUnlock() 1308 1309 // maker sell: base swap, quote redeem 1310 // taker buy: quote swap, base redeem 1311 1312 // maker buy: quote swap, base redeem 1313 // taker sell: base swap, quote redeem 1314 1315 // Maker broadcasts the swap contract. Sequence: NewlyMatched -> 1316 // MakerSwapCast -> TakerSwapCast -> MakerRedeemed -> MatchComplete 1317 switch match.Status { 1318 case order.NewlyMatched, order.TakerSwapCast: 1319 counterParty.order, actor.order = taker, maker 1320 actor.status = match.makerStatus 1321 counterParty.status = match.takerStatus 1322 actor.user = maker.User() 1323 counterParty.user = taker.User() 1324 actor.isMaker = true 1325 if match.Status == order.NewlyMatched { 1326 nextStep = order.MakerSwapCast 1327 isBaseAsset = maker.Sell // maker swap: base asset if sell 1328 if len(match.Sigs.MakerMatch) == 0 { 1329 log.Debugf("swap %v at status %v missing MakerMatch signature(s) expected before NewlyMatched->MakerSwapCast", 1330 match.ID(), match.Status) 1331 } 1332 } else /* TakerSwapCast */ { 1333 nextStep = order.MakerRedeemed 1334 isBaseAsset = !maker.Sell // maker redeem: base asset if buy 1335 if len(match.Sigs.MakerAudit) == 0 { 1336 log.Debugf("Swap %v at status %v missing MakerAudit signature(s) expected before TakerSwapCast->MakerRedeemed", 1337 match.ID(), match.Status) 1338 } 1339 } 1340 case order.MakerSwapCast, order.MakerRedeemed: 1341 counterParty.order, actor.order = maker, taker 1342 actor.status = match.takerStatus 1343 counterParty.status = match.makerStatus 1344 actor.user = taker.User() 1345 counterParty.user = maker.User() 1346 counterParty.isMaker = true 1347 if match.Status == order.MakerSwapCast { 1348 nextStep = order.TakerSwapCast 1349 isBaseAsset = !maker.Sell // taker swap: base asset if sell (maker buy) 1350 if len(match.Sigs.TakerMatch) == 0 { 1351 log.Debugf("Swap %v at status %v missing TakerMatch signature(s) expected before MakerSwapCast->TakerSwapCast", 1352 match.ID(), match.Status) 1353 } 1354 if len(match.Sigs.TakerAudit) == 0 { 1355 log.Debugf("Swap %v at status %v missing TakerAudit signature(s) expected before MakerSwapCast->TakerSwapCast", 1356 match.ID(), match.Status) 1357 } 1358 } else /* MakerRedeemed */ { 1359 nextStep = order.MatchComplete 1360 // Note that the swap is still considered "active" until both 1361 // counterparties acknowledge the redemptions. 1362 isBaseAsset = maker.Sell // taker redeem: base asset if buy (maker sell) 1363 if len(match.Sigs.TakerRedeem) == 0 { 1364 log.Debugf("Swap %v at status %v missing TakerRedeem signature(s) expected before MakerRedeemed->MatchComplete", 1365 match.ID(), match.Status) 1366 } 1367 } 1368 default: 1369 return nil, &msgjson.Error{ 1370 Code: msgjson.SettlementSequenceError, 1371 Message: "unknown settlement sequence identifier", 1372 } 1373 } 1374 1375 // Verify that the user specified is the actor for this step. 1376 if actor.user != user { // NOTE: self-trade slips past this 1377 return nil, &msgjson.Error{ 1378 Code: msgjson.SettlementSequenceError, 1379 Message: "expected other party to act", 1380 } 1381 } 1382 1383 // Set the actors' swapAsset and the swap contract checkVal. 1384 var checkVal uint64 1385 if isBaseAsset { 1386 actor.swapAsset = maker.BaseAsset 1387 counterParty.swapAsset = maker.QuoteAsset 1388 checkVal = match.Quantity 1389 } else { 1390 actor.swapAsset = maker.QuoteAsset 1391 counterParty.swapAsset = maker.BaseAsset 1392 checkVal = matcher.BaseToQuote(maker.Rate, match.Quantity) 1393 } 1394 1395 return &stepInformation{ 1396 match: match, 1397 actor: actor, 1398 counterParty: counterParty, 1399 // By the time a match is created, the presence of the asset in the map 1400 // has already been verified. 1401 asset: s.coins[actor.swapAsset].BackedAsset, 1402 isBaseAsset: isBaseAsset, 1403 step: match.Status, 1404 nextStep: nextStep, 1405 checkVal: checkVal, 1406 }, nil 1407 } 1408 1409 // authUser verifies that the msgjson.Signable is signed by the user. This 1410 // method relies on the AuthManager to validate the signature of the serialized 1411 // data. nil is returned for successful signature verification. 1412 func (s *Swapper) authUser(user account.AccountID, params msgjson.Signable) *msgjson.Error { 1413 // Authorize the user. 1414 msg := params.Serialize() 1415 err := s.authMgr.Auth(user, msg, params.SigBytes()) 1416 if err != nil { 1417 return &msgjson.Error{ 1418 Code: msgjson.SignatureError, 1419 Message: "error authenticating init params", 1420 } 1421 } 1422 return nil 1423 } 1424 1425 // messageAcker is information needed to process the user's 1426 // msgjson.Acknowledgement. 1427 type messageAcker struct { 1428 user account.AccountID 1429 match *matchTracker 1430 params msgjson.Signable 1431 isMaker bool 1432 isAudit bool 1433 } 1434 1435 // processAck processes a msgjson.Acknowledgement to the audit, redemption, and 1436 // revoke_match requests, validating the signature and updating the 1437 // (order.Match).Sigs record. This is required by processInit, processRedeem, 1438 // and revoke. Match Acknowledgements are handled by processMatchAck. 1439 func (s *Swapper) processAck(msg *msgjson.Message, acker *messageAcker) { 1440 ack := new(msgjson.Acknowledgement) 1441 err := msg.UnmarshalResult(ack) 1442 if err != nil { 1443 s.respondError(msg.ID, acker.user, msgjson.RPCParseError, fmt.Sprintf("error parsing acknowledgment: %v", err)) 1444 return 1445 } 1446 // Note: ack.MatchID unused, but could be checked against acker.match.ID(). 1447 1448 // Check the signature. 1449 sigMsg := acker.params.Serialize() 1450 err = s.authMgr.Auth(acker.user, sigMsg, ack.Sig) 1451 if err != nil { 1452 s.respondError(msg.ID, acker.user, msgjson.SignatureError, 1453 fmt.Sprintf("signature validation error: %v", err)) 1454 return 1455 } 1456 1457 switch acker.params.(type) { 1458 case *msgjson.Audit, *msgjson.Redemption: 1459 default: 1460 log.Warnf("unrecognized ack type %T", acker.params) 1461 return 1462 } 1463 1464 // Set and store the appropriate signature, based on the current step and 1465 // actor. 1466 mktMatch := db.MatchID(acker.match.Match) 1467 1468 // If this is the maker's (optional) redeem ack sig, we can stop tracking 1469 // the match. Do it here to avoid lock order violation (a deadlock trap). 1470 if acker.isMaker && !acker.isAudit { // getting Sigs.MakerRedeem 1471 log.Debugf("Deleting completed match %v", mktMatch) 1472 s.matchMtx.Lock() // before locking matchTracker.mtx 1473 s.deleteMatch(acker.match) 1474 s.matchMtx.Unlock() 1475 } 1476 1477 acker.match.mtx.Lock() 1478 defer acker.match.mtx.Unlock() 1479 1480 // This is an ack of either contract audit or redemption receipt. 1481 if acker.isAudit { 1482 log.Debugf("Received contract 'audit' acknowledgement from user %v (%s) for match %v (%v)", 1483 acker.user, makerTaker(acker.isMaker), acker.match.Match.ID(), acker.match.Status) 1484 // It's a contract audit ack. 1485 if acker.isMaker { 1486 acker.match.Sigs.MakerAudit = ack.Sig // i.e. audited taker's contract 1487 s.storage.SaveAuditAckSigA(mktMatch, ack.Sig) // sql error makes backend go fatal 1488 } else { 1489 acker.match.Sigs.TakerAudit = ack.Sig 1490 s.storage.SaveAuditAckSigB(mktMatch, ack.Sig) 1491 } 1492 return 1493 } 1494 1495 // It's a redemption ack. 1496 log.Debugf("Received 'redemption' acknowledgement from user %v (%s) for match %v (%s)", 1497 acker.user, makerTaker(acker.isMaker), acker.match.Match.ID(), acker.match.Status) 1498 1499 // This is a redemption acknowledgement. Store the ack signature, and 1500 // potentially record the order as complete with the auth manager and in 1501 // persistent storage. 1502 1503 // Record the taker's redeem ack sig. One from the maker isn't required. 1504 if acker.isMaker { // maker acknowledging the redeem req we sent regarding the taker redeem 1505 acker.match.Sigs.MakerRedeem = ack.Sig 1506 // We don't save that pointless sig anymore; use it as a flag. 1507 } else { // taker acknowledging the redeem req we sent regarding the maker redeem 1508 acker.match.Sigs.TakerRedeem = ack.Sig 1509 if err = s.storage.SaveRedeemAckSigB(mktMatch, ack.Sig); err != nil { 1510 s.respondError(msg.ID, acker.user, msgjson.RPCInternalError, 1511 "internal server error") 1512 log.Errorf("SaveRedeemAckSigB failed for match %v: %v", mktMatch.String(), err) 1513 return 1514 } 1515 } 1516 } 1517 1518 // processInit processes the `init` RPC request, which is used to inform the DEX 1519 // of a newly broadcast swap transaction. Once the transaction is seen and 1520 // audited by the Swapper, the counter-party is informed with an 'audit' 1521 // request. This method is run as a coin waiter, hence the return value 1522 // indicates if future attempts should be made to check coin status. 1523 func (s *Swapper) processInit(msg *msgjson.Message, params *msgjson.Init, stepInfo *stepInformation) wait.TryDirective { 1524 actor, counterParty := stepInfo.actor, stepInfo.counterParty 1525 1526 // Validate the swap contract 1527 chain := stepInfo.asset.Backend 1528 contract, err := chain.Contract(params.CoinID, params.Contract) 1529 if err != nil { 1530 if errors.Is(err, asset.CoinNotFoundError) { 1531 return wait.TryAgain 1532 } 1533 actor.status.mtx.RLock() 1534 log.Warnf("Contract error encountered for match %s, actor %s using coin ID %v and contract %v: %v", 1535 stepInfo.match.ID(), actor, params.CoinID, params.Contract, err) 1536 actor.status.mtx.RUnlock() 1537 actor.status.endSwapSearch() // allow client retry even before notifying him 1538 s.respondError(msg.ID, actor.user, msgjson.ContractError, 1539 fmt.Sprintf("contract error encountered: %v", err)) 1540 return wait.DontTryAgain 1541 } 1542 1543 // Enforce the prescribed swap fee rate, but only if the swap is not already 1544 // confirmed. 1545 swapConfs := func() int64 { // not executed if adequate fee rate 1546 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 1547 defer cancel() 1548 confs, err := contract.Confirmations(ctx) 1549 if err != nil { 1550 log.Warnf("Failed to get confirmations on swap tx %v: %v", contract.TxID(), err) 1551 confs = 0 // should be already 1552 } 1553 return confs 1554 } 1555 reqFeeRate := stepInfo.match.FeeRateQuote 1556 if stepInfo.isBaseAsset { 1557 reqFeeRate = stepInfo.match.FeeRateBase 1558 } 1559 1560 if !chain.ValidateFeeRate(contract.Coin, reqFeeRate) { 1561 confs := swapConfs() 1562 if confs < 1 { 1563 actor.status.endSwapSearch() // allow client retry even before notifying him 1564 s.respondError(msg.ID, actor.user, msgjson.ContractError, "low tx fee") 1565 return wait.DontTryAgain 1566 } 1567 log.Infof("Swap txn %v (%s) with low fee rate (%v required), accepted with %d confirmations.", 1568 contract, stepInfo.asset.Symbol, reqFeeRate, confs) 1569 } 1570 if contract.SwapAddress != counterParty.order.Trade().SwapAddress() { 1571 actor.status.endSwapSearch() // allow client retry even before notifying him 1572 s.respondError(msg.ID, actor.user, msgjson.ContractError, 1573 fmt.Sprintf("incorrect recipient. expected %s. got %s", 1574 contract.SwapAddress, counterParty.order.Trade().SwapAddress())) 1575 return wait.DontTryAgain 1576 } 1577 if contract.Value() != stepInfo.checkVal { 1578 actor.status.endSwapSearch() // allow client retry even before notifying him 1579 s.respondError(msg.ID, actor.user, msgjson.ContractError, 1580 fmt.Sprintf("contract error. expected contract value to be %d, got %d", stepInfo.checkVal, contract.Value())) 1581 return wait.DontTryAgain 1582 } 1583 if !actor.isMaker && !bytes.Equal(contract.SecretHash, counterParty.status.swap.SecretHash) { 1584 actor.status.endSwapSearch() // allow client retry even before notifying him 1585 s.respondError(msg.ID, actor.user, msgjson.ContractError, 1586 fmt.Sprintf("incorrect secret hash. expected %x. got %x", 1587 contract.SecretHash, counterParty.status.swap.SecretHash)) 1588 return wait.DontTryAgain 1589 } 1590 1591 reqLockTime := encode.DropMilliseconds(stepInfo.match.matchTime.Add(s.lockTimeTaker)) 1592 if actor.isMaker { 1593 reqLockTime = encode.DropMilliseconds(stepInfo.match.matchTime.Add(s.lockTimeMaker)) 1594 } 1595 if contract.LockTime.Before(reqLockTime) { 1596 actor.status.endSwapSearch() // allow client retry even before notifying him 1597 s.respondError(msg.ID, actor.user, msgjson.ContractError, 1598 fmt.Sprintf("contract error. expected lock time >= %s, got %s", reqLockTime, contract.LockTime)) 1599 return wait.DontTryAgain 1600 } else if remain := time.Until(contract.LockTime); remain < 0 { 1601 actor.status.endSwapSearch() // allow client retry even before notifying him 1602 s.respondError(msg.ID, actor.user, msgjson.ContractError, 1603 fmt.Sprintf("contract is correct, but lock time passed %s ago", remain)) 1604 // Revoke the match proactively before checkInaction gets to it. 1605 s.matchMtx.Lock() 1606 defer s.matchMtx.Unlock() 1607 if _, found := s.matches[stepInfo.match.ID()]; found { 1608 s.failMatch(stepInfo.match, false) // no fault 1609 s.deleteMatch(stepInfo.match) 1610 } // else it's already revoked 1611 return wait.DontTryAgain // and don't tell counterparty of expired contract they should not redeem 1612 } 1613 1614 // Update the match. 1615 swapTime := unixMsNow() 1616 matchID := stepInfo.match.Match.ID() 1617 1618 // Store the swap contract and the coinID (e.g. txid:vout) containing the 1619 // contract script hash. Maker is party A, the initiator. Taker is party B, 1620 // the participant. 1621 // 1622 // Failure to update match status is a fatal DB error. If we continue, the 1623 // swap may succeed, but there will be no way to recover or retrospectively 1624 // determine the swap outcome. Abort. 1625 storFn := s.storage.SaveContractB 1626 if stepInfo.actor.isMaker { 1627 storFn = s.storage.SaveContractA 1628 } 1629 mktMatch := db.MatchID(stepInfo.match.Match) 1630 swapTimeMs := swapTime.UnixMilli() 1631 err = storFn(mktMatch, params.Contract, params.CoinID, swapTimeMs) 1632 if err != nil { 1633 log.Errorf("saving swap contract (match id=%v, maker=%v) failed: %v", 1634 matchID, actor.isMaker, err) 1635 s.respondError(msg.ID, actor.user, msgjson.RPCInternalError, 1636 "internal server error") 1637 // TODO: revoke the match without penalties instead of retrying forever? 1638 return wait.TryAgain 1639 } 1640 1641 // Modify the match's swapStatuses, but only if the match wasn't revoked 1642 // while waiting for the txn. 1643 s.matchMtx.RLock() 1644 if _, found := s.matches[matchID]; !found { 1645 s.matchMtx.RUnlock() 1646 log.Errorf("Contract txn located after match was revoked (match id=%v, maker=%v)", 1647 matchID, actor.isMaker) 1648 actor.status.endSwapSearch() // allow client retry even before notifying him 1649 s.respondError(msg.ID, actor.user, msgjson.ContractError, "match already revoked due to inaction") 1650 return wait.DontTryAgain 1651 } 1652 1653 actor.status.mtx.Lock() 1654 actor.status.swap = contract // swap should not be set already (handleInit should gate) 1655 actor.status.swapTime = swapTime 1656 actor.status.mtx.Unlock() 1657 1658 stepInfo.match.mtx.Lock() 1659 stepInfo.match.Status = stepInfo.nextStep // handleInit (gate mechanism) won't allow backward progress 1660 stepInfo.match.mtx.Unlock() 1661 1662 // Only unlock match map after the statuses and txn times are stored, 1663 // ensuring that checkInaction will not revoke the match as we respond and 1664 // request counterparty audit. 1665 s.matchMtx.RUnlock() 1666 1667 // Contract now recorded and will be used to reject backward progress (duplicate 1668 // or malicious requests client might still send after this point). 1669 actor.status.endSwapSearch() 1670 1671 log.Debugf("processInit: valid contract %v (%s) received at %v from user %v (%s) for match %v, "+ 1672 "swapStatus %v => %v", contract, stepInfo.asset.Symbol, swapTime, actor.user, 1673 makerTaker(actor.isMaker), matchID, stepInfo.step, stepInfo.nextStep) 1674 1675 // Issue a positive response to the actor. 1676 s.authMgr.Sign(params) 1677 s.respondSuccess(msg.ID, actor.user, &msgjson.Acknowledgement{ 1678 MatchID: matchID[:], 1679 Sig: params.Sig, 1680 }) 1681 1682 // Prepare an 'audit' request for the counter-party. 1683 auditParams := &msgjson.Audit{ 1684 OrderID: idToBytes(counterParty.order.ID()), 1685 MatchID: matchID[:], 1686 Time: uint64(swapTimeMs), 1687 CoinID: params.CoinID, 1688 Contract: params.Contract, 1689 TxData: contract.TxData, 1690 } 1691 s.authMgr.Sign(auditParams) 1692 notification, err := msgjson.NewRequest(comms.NextID(), msgjson.AuditRoute, auditParams) 1693 if err != nil { 1694 // This is likely an impossible condition. 1695 log.Errorf("error creating audit request: %v", err) 1696 return wait.DontTryAgain 1697 } 1698 1699 // Set up the acknowledgement for the callback. 1700 ack := &messageAcker{ 1701 user: counterParty.user, 1702 match: stepInfo.match, 1703 params: auditParams, 1704 isMaker: counterParty.isMaker, 1705 isAudit: true, 1706 } 1707 // Send the 'audit' request to the counter-party. 1708 log.Debugf("processInit: sending contract 'audit' request to counterparty %v (%s) "+ 1709 "for match %v", ack.user, makerTaker(ack.isMaker), matchID) 1710 // The counterparty will audit the contract by retrieving it, which may 1711 // involve them waiting for up to the broadcast timeout before responding, 1712 // so the user gets at least s.bTimeout to the request. 1713 err = s.authMgr.RequestWithTimeout(ack.user, notification, func(_ comms.Link, resp *msgjson.Message) { 1714 s.processAck(resp, ack) // resp.ID == notification.ID 1715 }, s.bTimeout, func() { 1716 log.Infof("Timeout waiting for contract 'audit' request acknowledgement from user %v (%s) for match %v", 1717 ack.user, makerTaker(ack.isMaker), matchID) 1718 }) 1719 if err != nil { 1720 log.Debug("Couldn't send 'audit' request to user %v (%s) for match %v", ack.user, makerTaker(ack.isMaker), matchID) 1721 } 1722 1723 return wait.DontTryAgain 1724 } 1725 1726 // processRedeem processes a 'redeem' request from a client. processRedeem does 1727 // not perform user authentication, which is handled in handleRedeem before 1728 // processRedeem is invoked. This method is run as a coin waiter. 1729 func (s *Swapper) processRedeem(msg *msgjson.Message, params *msgjson.Redeem, stepInfo *stepInformation) wait.TryDirective { 1730 // TODO(consider): Extract secret from initiator's (maker's) redemption 1731 // transaction. The Backend would need a method identify the component of 1732 // the redemption transaction that contains the secret and extract it. In a 1733 // UTXO-based asset, this means finding the input that spends the output of 1734 // the counterparty's contract, and process that input's signature script 1735 // with FindKeyPush. Presently this is up to the clients and not stored with 1736 // the server. 1737 1738 // Make sure that the expected output is being spent. 1739 actor, counterParty := stepInfo.actor, stepInfo.counterParty 1740 counterParty.status.mtx.RLock() 1741 cpContract := counterParty.status.swap.ContractData 1742 cpSwapCoin := counterParty.status.swap.ID() 1743 cpSwapStr := counterParty.status.swap.String() 1744 counterParty.status.mtx.RUnlock() 1745 1746 // Get the transaction. 1747 match := stepInfo.match 1748 matchID := match.ID() 1749 chain := stepInfo.asset.Backend 1750 if !chain.ValidateSecret(params.Secret, cpContract) { 1751 log.Infof("Secret validation failed (match id=%v, maker=%v, secret=%v)", 1752 matchID, actor.isMaker, params.Secret) 1753 actor.status.endRedeemSearch() // allow client retry even before notifying him 1754 s.respondError(msg.ID, actor.user, msgjson.InvalidRequestError, "secret validation failed") 1755 return wait.DontTryAgain 1756 } 1757 redemption, err := chain.Redemption(params.CoinID, cpSwapCoin, cpContract) 1758 // If there is an error, don't return an error yet, since it could be due to 1759 // network latency. Instead, queue it up for another check. 1760 if err != nil { 1761 if errors.Is(err, asset.CoinNotFoundError) { 1762 return wait.TryAgain 1763 } 1764 actor.status.mtx.RLock() 1765 log.Warnf("Redemption error encountered for match %s, actor %s, using coin ID %v to satisfy contract at %x: %v", 1766 stepInfo.match.ID(), actor, params.CoinID, cpSwapCoin, err) 1767 actor.status.mtx.RUnlock() 1768 actor.status.endRedeemSearch() // allow client retry even before notifying him 1769 s.respondError(msg.ID, actor.user, msgjson.RedemptionError, 1770 fmt.Sprintf("redemption error encountered: %v", err)) 1771 return wait.DontTryAgain 1772 } 1773 1774 newStatus := stepInfo.nextStep 1775 1776 // NOTE: redemption.FeeRate is not checked since the counterparty is not 1777 // inconvenienced by slow confirmation of the redemption. 1778 1779 // Modify the match's swapStatuses, but only if the match wasn't revoked 1780 // while waiting for the txn. 1781 s.matchMtx.RLock() 1782 if _, found := s.matches[matchID]; !found { 1783 s.matchMtx.RUnlock() 1784 log.Errorf("Redeem txn found after match was revoked (match id=%v, maker=%v)", 1785 matchID, actor.isMaker) 1786 actor.status.endRedeemSearch() // allow client retry even before notifying him 1787 s.respondError(msg.ID, actor.user, msgjson.RedemptionError, "match already revoked due to inaction") 1788 return wait.DontTryAgain 1789 } 1790 1791 actor.status.mtx.Lock() 1792 redeemTime := unixMsNow() 1793 actor.status.redemption = redemption // redemption should not be set already (handleRedeem should gate) 1794 actor.status.redeemTime = redeemTime 1795 actor.status.mtx.Unlock() 1796 1797 match.mtx.Lock() 1798 match.Status = newStatus // handleRedeem (gate mechanism) won't allow backward progress 1799 match.mtx.Unlock() 1800 1801 // Only unlock match map after the statuses and txn times are stored, 1802 // ensuring that checkInaction will not revoke the match as we respond. 1803 s.matchMtx.RUnlock() 1804 1805 // Redemption now recorded and will be used to reject backward progress (duplicate 1806 // or malicious requests client might still send after this point). 1807 actor.status.endRedeemSearch() 1808 1809 log.Debugf("processRedeem: valid redemption %v (%s) spending contract %s received at %v from %v (%s) for match %v, "+ 1810 "swapStatus %v => %v", redemption, stepInfo.asset.Symbol, cpSwapStr, redeemTime, actor.user, 1811 makerTaker(actor.isMaker), matchID, stepInfo.step, newStatus) 1812 // If MatchComplete, we'll delete the match in processAck if the maker 1813 // responds to the courtesy redemption request, or checkInactionEventBased. 1814 1815 // Store the swap contract and the coinID (e.g. txid:vout) containing the 1816 // contract script hash. Maker is party A, the initiator, who first reveals 1817 // the secret. Taker is party B, the participant. 1818 storFn := s.storage.SaveRedeemB // taker's redeem also sets match status to MatchComplete, active to FALSE 1819 if actor.isMaker { 1820 // Maker redeem stores the secret too. 1821 storFn = func(mid db.MarketMatchID, coinID []byte, timestamp int64) error { 1822 return s.storage.SaveRedeemA(mid, coinID, params.Secret, timestamp) // also sets match status to MakerRedeemed 1823 } 1824 } 1825 1826 redeemTimeMs := redeemTime.UnixMilli() 1827 err = storFn(db.MatchID(match.Match), params.CoinID, redeemTimeMs) 1828 if err != nil { 1829 log.Errorf("saving redeem transaction (match id=%v, maker=%v) failed: %v", 1830 matchID, actor.isMaker, err) 1831 // Neither party's fault. Continue. 1832 } 1833 1834 // Credit the user for completing the swap, adjusting the user's score. 1835 if actor.user != counterParty.user { 1836 s.authMgr.SwapSuccess(actor.user, db.MatchID(match.Match), match.Quantity, redeemTime) // maybe call this in swapDone callback 1837 } 1838 1839 // Issue a positive response to the actor. 1840 s.authMgr.Sign(params) 1841 s.respondSuccess(msg.ID, actor.user, &msgjson.Acknowledgement{ 1842 MatchID: matchID[:], 1843 Sig: params.Sig, 1844 }) 1845 1846 // Cancellation rate accounting 1847 ord := match.Taker 1848 if actor.isMaker { 1849 ord = match.Maker 1850 } 1851 1852 s.swapDone(ord, match.Match, false) 1853 1854 // Inform the counterparty, even though the maker doesn't really care about 1855 // the taker's redeem details. 1856 rParams := &msgjson.Redemption{ 1857 Redeem: msgjson.Redeem{ 1858 OrderID: idToBytes(counterParty.order.ID()), 1859 MatchID: matchID[:], 1860 CoinID: params.CoinID, 1861 Secret: params.Secret, 1862 }, 1863 Time: uint64(redeemTimeMs), 1864 } 1865 s.authMgr.Sign(rParams) 1866 redemptionReq, err := msgjson.NewRequest(comms.NextID(), msgjson.RedemptionRoute, rParams) 1867 if err != nil { 1868 log.Errorf("error creating redemption request: %v", err) 1869 return wait.DontTryAgain 1870 } 1871 1872 // Send the redemption request. 1873 log.Debugf("processRedeem: sending 'redemption' request to counterparty %v (%s) "+ 1874 "for match %v", counterParty.user, makerTaker(counterParty.isMaker), matchID) 1875 1876 // Set up the redemption acknowledgement callback. 1877 ack := &messageAcker{ 1878 user: counterParty.user, 1879 match: match, 1880 params: rParams, 1881 isMaker: counterParty.isMaker, 1882 // isAudit: false, 1883 } 1884 1885 // The counterparty does not need to actually locate the redemption txn, 1886 // so use the default request timeout. 1887 err = s.authMgr.RequestWithTimeout(ack.user, redemptionReq, func(_ comms.Link, resp *msgjson.Message) { 1888 s.processAck(resp, ack) // resp.ID == notification.ID 1889 }, time.Until(redeemTime.Add(s.bTimeout)), func() { 1890 log.Infof("Timeout waiting for 'redemption' request from user %v (%s) for match %v", 1891 ack.user, makerTaker(ack.isMaker), matchID) 1892 }) 1893 if err != nil { 1894 log.Debugf("Couldn't send 'redemption' request to user %v (%s) for match %v", ack.user, makerTaker(ack.isMaker), matchID) 1895 } 1896 1897 return wait.DontTryAgain 1898 } 1899 1900 // handleInit handles the 'init' request from a user, which is used to inform 1901 // the DEX of a newly broadcast swap transaction. The Init message includes the 1902 // swap contract script and the CoinID of contract. Most of the work is 1903 // performed by processInit, but the request is parsed and user is authenticated 1904 // first. 1905 func (s *Swapper) handleInit(user account.AccountID, msg *msgjson.Message) *msgjson.Error { 1906 s.handlerMtx.RLock() 1907 defer s.handlerMtx.RUnlock() // block shutdown until registered with latencyQ 1908 if s.stop { 1909 return &msgjson.Error{ 1910 Code: msgjson.TryAgainLaterError, 1911 Message: "The swapper is stopping. Try again later.", 1912 } 1913 } 1914 1915 params := new(msgjson.Init) 1916 err := msg.Unmarshal(¶ms) 1917 if err != nil || params == nil { 1918 return &msgjson.Error{ 1919 Code: msgjson.RPCParseError, 1920 Message: "Error decoding 'init' method params", 1921 } 1922 } 1923 1924 // Verify the user's signature of params. 1925 rpcErr := s.authUser(user, params) 1926 if rpcErr != nil { 1927 return rpcErr 1928 } 1929 1930 log.Debugf("handleInit: 'init' received from user %v for match %v, order %v", 1931 user, params.MatchID, params.OrderID) 1932 1933 if len(params.MatchID) != order.MatchIDSize { 1934 return &msgjson.Error{ 1935 Code: msgjson.RPCParseError, 1936 Message: "Invalid 'matchid' in 'init' message", 1937 } 1938 } 1939 1940 var matchID order.MatchID 1941 copy(matchID[:], params.MatchID) 1942 stepInfo, rpcErr := s.step(user, matchID) 1943 if rpcErr != nil { 1944 return rpcErr 1945 } 1946 1947 // init requests should only be sent when contracts are still required, in 1948 // the correct sequence, and by the correct party. 1949 switch stepInfo.step { 1950 case order.NewlyMatched, order.MakerSwapCast: 1951 // Ensure we only start one coin waiter for this swap. This is an atomic 1952 // CAS, so it must ultimately be followed by endSwapSearch(). 1953 if !stepInfo.actor.status.startSwapSearch() { 1954 return &msgjson.Error{ 1955 Code: msgjson.DuplicateRequestError, // not really a sequence error since they are still the "actor" 1956 Message: "already received a swap contract, search in progress", 1957 } 1958 } 1959 default: 1960 return &msgjson.Error{ 1961 Code: msgjson.SettlementSequenceError, 1962 Message: "swap contract already provided", 1963 } 1964 } 1965 1966 // Validate the coinID and contract script before starting a coin waiter. 1967 coinStr, err := stepInfo.asset.Backend.ValidateCoinID(params.CoinID) 1968 if err != nil { 1969 stepInfo.actor.status.endSwapSearch() // not gonna start the search 1970 // TODO: ensure Backends provide sanitized errors or type information to 1971 // provide more details to the client. 1972 return &msgjson.Error{ 1973 Code: msgjson.ContractError, 1974 Message: "invalid contract coinID or script", 1975 } 1976 } 1977 err = stepInfo.asset.Backend.ValidateContract(params.Contract) 1978 if err != nil { 1979 stepInfo.actor.status.endSwapSearch() // not gonna start the search 1980 log.Debugf("ValidateContract (asset %v, coin %v) failure: %v", stepInfo.asset.Symbol, coinStr, err) 1981 // TODO: ensure Backends provide sanitized errors or type information to 1982 // provide more details to the client. 1983 return &msgjson.Error{ 1984 Code: msgjson.ContractError, 1985 Message: "invalid swap contract", 1986 } 1987 } 1988 1989 // TODO: consider also checking recipient of contract here, but it is also 1990 // checked in processInit. Note that value cannot be checked as transaction 1991 // details, which includes the coin/output value, are not yet retrieved. 1992 1993 // Search for the transaction for the full txWaitExpiration, even if it goes 1994 // past the inaction deadline. processInit recognizes when it is revoked. 1995 expireTime := time.Now().Add(s.txWaitExpiration).UTC() 1996 log.Debugf("Allowing until %v (%v) to locate contract from %v (%v), match %v, tx %s (%s)", 1997 expireTime, time.Until(expireTime), makerTaker(stepInfo.actor.isMaker), 1998 stepInfo.step, matchID, coinStr, stepInfo.asset.Symbol) 1999 2000 // Since we have to consider broadcast latency of the asset's network, run 2001 // this as a coin waiter. 2002 s.latencyQ.Wait(&wait.Waiter{ 2003 Expiration: expireTime, 2004 TryFunc: func() wait.TryDirective { 2005 return s.processInit(msg, params, stepInfo) 2006 }, 2007 ExpireFunc: func() { 2008 stepInfo.actor.status.endSwapSearch() // allow init retries 2009 // NOTE: We may consider a shorter expire time so the client can 2010 // receive warning that there may be node or wallet connectivity 2011 // trouble while they still have a chance to fix it. 2012 s.respondError(msg.ID, user, msgjson.TransactionUndiscovered, 2013 fmt.Sprintf("failed to find contract coin %v", coinStr)) 2014 }, 2015 }) 2016 return nil 2017 } 2018 2019 // handleRedeem handles the 'redeem' request from a user. Most of the work is 2020 // performed by processRedeem, but the request is parsed and user is 2021 // authenticated first. 2022 func (s *Swapper) handleRedeem(user account.AccountID, msg *msgjson.Message) *msgjson.Error { 2023 s.handlerMtx.RLock() 2024 defer s.handlerMtx.RUnlock() // block shutdown until registered with latencyQ 2025 if s.stop { 2026 return &msgjson.Error{ 2027 Code: msgjson.TryAgainLaterError, 2028 Message: "The swapper is stopping. Try again later.", 2029 } 2030 } 2031 2032 params := new(msgjson.Redeem) 2033 err := msg.Unmarshal(¶ms) 2034 if err != nil || params == nil { 2035 return &msgjson.Error{ 2036 Code: msgjson.RPCParseError, 2037 Message: "Error decoding 'redeem' request payload", 2038 } 2039 } 2040 2041 rpcErr := s.authUser(user, params) 2042 if rpcErr != nil { 2043 return rpcErr 2044 } 2045 2046 log.Debugf("handleRedeem: 'redeem' received from %v for match %v, order %v", 2047 user, params.MatchID, params.OrderID) 2048 2049 if len(params.MatchID) != order.MatchIDSize { 2050 return &msgjson.Error{ 2051 Code: msgjson.RPCParseError, 2052 Message: "Invalid 'matchid' in 'redeem' message", 2053 } 2054 } 2055 2056 var matchID order.MatchID 2057 copy(matchID[:], params.MatchID) 2058 stepInfo, rpcErr := s.step(user, matchID) 2059 if rpcErr != nil { 2060 return rpcErr 2061 } 2062 2063 // redeem requests should only be sent when all contracts have been 2064 // received, in the correct sequence, and by the correct party. 2065 switch stepInfo.step { 2066 case order.TakerSwapCast, order.MakerRedeemed: 2067 // Ensure we only start one coin waiter for this redeem. This is an 2068 // atomic CAS, so it must ultimately be followed by endRedeemSearch(). 2069 if !stepInfo.actor.status.startRedeemSearch() { 2070 return &msgjson.Error{ 2071 Code: msgjson.DuplicateRequestError, // not really a sequence error since they are still the "actor" 2072 Message: "already received a redeem transaction, search in progress", 2073 } 2074 } 2075 default: // also includes MatchComplete 2076 return &msgjson.Error{ 2077 Code: msgjson.SettlementSequenceError, 2078 Message: "swap contracts not yet received", 2079 } 2080 } 2081 2082 // Validate the redeem coin ID before starting a wait. This does not 2083 // check the blockchain, but does ensure the CoinID can be decoded for the 2084 // asset before starting up a coin waiter. 2085 coinStr, err := stepInfo.asset.Backend.ValidateCoinID(params.CoinID) 2086 if err != nil { 2087 stepInfo.actor.status.endRedeemSearch() 2088 // TODO: ensure Backends provide sanitized errors or type information to 2089 // provide more details to the client. 2090 return &msgjson.Error{ 2091 Code: msgjson.ContractError, 2092 Message: "invalid 'redeem' parameters", 2093 } 2094 } 2095 2096 // Search for the transaction for the full txWaitExpiration, even if it goes 2097 // past the inaction deadline. processRedeem recognizes when it is revoked. 2098 expireTime := time.Now().Add(s.txWaitExpiration).UTC() 2099 log.Debugf("Allowing until %v (%v) to locate redeem from %v (%v), match %v, tx %s (%s)", 2100 expireTime, time.Until(expireTime), makerTaker(stepInfo.actor.isMaker), 2101 stepInfo.step, matchID, coinStr, stepInfo.asset.Symbol) 2102 2103 // Since we have to consider latency, run this as a coin waiter. 2104 s.latencyQ.Wait(&wait.Waiter{ 2105 Expiration: expireTime, 2106 TryFunc: func() wait.TryDirective { 2107 return s.processRedeem(msg, params, stepInfo) 2108 }, 2109 ExpireFunc: func() { 2110 stepInfo.actor.status.endRedeemSearch() 2111 // NOTE: We may consider a shorter expire time so the client can 2112 // receive warning that there may be node or wallet connectivity 2113 // trouble while they still have a chance to fix it. 2114 s.respondError(msg.ID, user, msgjson.TransactionUndiscovered, 2115 fmt.Sprintf("failed to find redeemed coin %v", coinStr)) 2116 }, 2117 }) 2118 return nil 2119 } 2120 2121 // revoke revokes the match, sending the 'revoke_match' request to each client 2122 // and processing the acknowledgement. Match Sigs and Status are not accessed. 2123 func (s *Swapper) revoke(match *matchTracker) { 2124 route := msgjson.RevokeMatchRoute 2125 log.Infof("Sending a '%s' notification to each client for match %v", 2126 route, match.ID()) 2127 2128 sendRev := func(mid order.MatchID, ord order.Order) { 2129 msg := &msgjson.RevokeMatch{ 2130 OrderID: ord.ID().Bytes(), 2131 MatchID: mid[:], 2132 } 2133 s.authMgr.Sign(msg) 2134 ntfn, err := msgjson.NewNotification(route, msg) 2135 if err != nil { 2136 log.Errorf("Failed to create '%s' notification for user %v, match %v: %v", 2137 route, ord.User(), mid, err) 2138 return 2139 } 2140 if err = s.authMgr.Send(ord.User(), ntfn); err != nil { 2141 log.Debugf("Failed to send '%s' notification to user %v, match %v: %v", 2142 route, ord.User(), mid, err) 2143 } 2144 } 2145 2146 mid := match.ID() 2147 sendRev(mid, match.Taker) 2148 sendRev(mid, match.Maker) 2149 } 2150 2151 // For the 'match' request, the user returns a msgjson.Acknowledgement array 2152 // with signatures for each match ID. The match acknowledgements were requested 2153 // from each matched user in Negotiate. 2154 func (s *Swapper) processMatchAcks(user account.AccountID, msg *msgjson.Message, matches []*messageAcker) { 2155 // NOTE: acks must be in same order as matches []*messageAcker. 2156 var acks []msgjson.Acknowledgement 2157 err := msg.UnmarshalResult(&acks) 2158 if err != nil { 2159 s.respondError(msg.ID, user, msgjson.RPCParseError, 2160 fmt.Sprintf("error parsing match request acknowledgment: %v", err)) 2161 return 2162 } 2163 if len(matches) != len(acks) { 2164 s.respondError(msg.ID, user, msgjson.AckCountError, 2165 fmt.Sprintf("expected %d acknowledgements, got %d", len(matches), len(acks))) 2166 return 2167 } 2168 2169 log.Debugf("processMatchAcks: 'match' ack received from %v for %d matches", 2170 user, len(matches)) 2171 2172 // Verify the signature of each Acknowledgement, and store the signatures in 2173 // the matchTracker of each match (messageAcker). The signature will be 2174 // either a MakerMatch or TakerMatch signature depending on whether the 2175 // responding user is the maker or taker. 2176 for i, matchInfo := range matches { 2177 ack := &acks[i] 2178 match := matchInfo.match 2179 2180 matchID := match.ID() 2181 if !bytes.Equal(ack.MatchID, matchID[:]) { 2182 s.respondError(msg.ID, user, msgjson.IDMismatchError, 2183 fmt.Sprintf("unexpected match ID at acknowledgment index %d", i)) 2184 return 2185 } 2186 sigMsg := matchInfo.params.Serialize() 2187 err = s.authMgr.Auth(user, sigMsg, ack.Sig) 2188 if err != nil { 2189 log.Warnf("processMatchAcks: 'match' ack for match %v from user %v, "+ 2190 " failed sig verification: %v", matchID, user, err) 2191 s.respondError(msg.ID, user, msgjson.SignatureError, 2192 fmt.Sprintf("signature validation error: %v", err)) 2193 return 2194 } 2195 2196 // Store the signature in the matchTracker. These must be collected 2197 // before the init steps begin and swap contracts are broadcasted. 2198 match.mtx.Lock() 2199 status := match.Status 2200 if matchInfo.isMaker { 2201 match.Sigs.MakerMatch = ack.Sig 2202 } else { 2203 match.Sigs.TakerMatch = ack.Sig 2204 } 2205 match.mtx.Unlock() 2206 log.Debugf("processMatchAcks: storing valid 'match' ack signature from %v (maker=%v) "+ 2207 "for match %v (status %v)", user, matchInfo.isMaker, matchID, status) 2208 } 2209 2210 // Store the signatures in the DB. 2211 for i, matchInfo := range matches { 2212 ackSig := acks[i].Sig 2213 match := matchInfo.match 2214 2215 storFn := s.storage.SaveMatchAckSigB 2216 if matchInfo.isMaker { 2217 storFn = s.storage.SaveMatchAckSigA 2218 } 2219 matchID := match.ID() 2220 mid := db.MarketMatchID{ 2221 MatchID: matchID, 2222 Base: match.Maker.BaseAsset, // same for taker's redeem as BaseAsset refers to the market 2223 Quote: match.Maker.QuoteAsset, 2224 } 2225 err = storFn(mid, ackSig) 2226 if err != nil { 2227 log.Errorf("saving match ack signature (match id=%v, maker=%v) failed: %v", 2228 matchID, matchInfo.isMaker, err) 2229 s.respondError(msg.ID, matchInfo.user, msgjson.UnknownMarketError, 2230 "internal server error") 2231 // TODO: revoke the match without penalties? 2232 return 2233 } 2234 } 2235 } 2236 2237 // CheckUnspent attempts to verify a coin ID for a given asset by retrieving the 2238 // corresponding asset.Coin. If the coin is not found or spent, an 2239 // asset.CoinNotFoundError is returned. CheckUnspent returns immediately with 2240 // no error if the requested asset is not a utxo-based asset. 2241 func (s *Swapper) CheckUnspent(ctx context.Context, assetID uint32, coinID []byte) error { 2242 backend := s.coins[assetID] 2243 if backend == nil { 2244 return fmt.Errorf("unknown asset %d", assetID) 2245 } 2246 outputTracker, is := backend.Backend.(asset.OutputTracker) 2247 if !is { 2248 return nil 2249 } 2250 return outputTracker.VerifyUnspentCoin(ctx, coinID) 2251 } 2252 2253 // LockOrdersCoins locks the backing coins for the provided orders. 2254 func (s *Swapper) LockOrdersCoins(orders []order.Order) { 2255 // Separate orders according to the asset of their locked coins. 2256 assetCoinOrders := make(map[uint32][]order.Order, len(orders)) 2257 for _, ord := range orders { 2258 // Identify the asset of the locked coins. 2259 asset := ord.Quote() 2260 if ord.Trade().Sell { 2261 asset = ord.Base() 2262 } 2263 assetCoinOrders[asset] = append(assetCoinOrders[asset], ord) 2264 } 2265 2266 for asset, orders := range assetCoinOrders { 2267 s.lockOrdersCoins(asset, orders) 2268 } 2269 } 2270 2271 func (s *Swapper) lockOrdersCoins(assetID uint32, orders []order.Order) { 2272 swapperAsset := s.coins[assetID] 2273 if swapperAsset == nil { 2274 log.Errorf(fmt.Sprintf("lockOrderCoins called for unknown asset %d", assetID)) 2275 return 2276 } 2277 if swapperAsset.Locker == nil { 2278 return 2279 } 2280 2281 swapperAsset.Locker.LockOrdersCoins(orders) 2282 } 2283 2284 // LockCoins locks coins of a given asset. The OrderID is used for tracking. 2285 func (s *Swapper) LockCoins(asset uint32, coins map[order.OrderID][]order.CoinID) { 2286 swapperAsset := s.coins[asset] 2287 if swapperAsset == nil { 2288 panic(fmt.Sprintf("Unable to lock coins for asset %d", asset)) 2289 } 2290 if swapperAsset.Locker == nil { 2291 return 2292 } 2293 2294 swapperAsset.Locker.LockCoins(coins) 2295 } 2296 2297 // unlockOrderCoins is not exported since only the Swapper knows when to unlock 2298 // coins (when funding coins are spent in a fully-confirmed contract). 2299 func (s *Swapper) unlockOrderCoins(ord order.Order) { 2300 assetID := ord.Quote() 2301 if ord.Trade().Sell { 2302 assetID = ord.Base() 2303 } 2304 2305 s.unlockOrderIDCoins(assetID, ord.ID()) 2306 } 2307 2308 func (s *Swapper) unlockOrderIDCoins(assetID uint32, oid order.OrderID) { 2309 swapperAsset := s.coins[assetID] 2310 if swapperAsset == nil { 2311 log.Errorf(fmt.Sprintf("unlockOrderIDCoins called for unknown asset %d", assetID)) 2312 return 2313 } 2314 if swapperAsset.Locker == nil { 2315 return 2316 } 2317 2318 swapperAsset.Locker.UnlockOrderCoins(oid) 2319 } 2320 2321 // matchNotifications creates a pair of msgjson.Match from a matchTracker. 2322 func matchNotifications(match *matchTracker) (makerMsg *msgjson.Match, takerMsg *msgjson.Match) { 2323 // NOTE: If we decide that msgjson.Match should just have a 2324 // "FeeRateBaseSwap" field, this could be set according to the 2325 // swapStatus.swapAsset field: 2326 // 2327 // base, quote := match.Maker.BaseAsset, match.Maker.QuoteAsset 2328 // feeRate := func(assetID uint32) uint64 { 2329 // if assetID == match.Maker.BaseAsset { 2330 // return match.FeeRateBase 2331 // } 2332 // return match.FeeRateQuote 2333 // } 2334 // FeeRateMakerSwap := feeRate(match.makerStatus.swapAsset) 2335 2336 // If the taker order is a cancel, omit the maker (trade) order's address 2337 // since it is dead weight. Consider omitting the numeric fields too. 2338 var makerAddr string 2339 if match.Taker.Type() != order.CancelOrderType { 2340 makerAddr = order.ExtractAddress(match.Maker) 2341 } 2342 2343 stamp := uint64(match.matchTime.UnixMilli()) 2344 return &msgjson.Match{ 2345 OrderID: idToBytes(match.Maker.ID()), 2346 MatchID: idToBytes(match.ID()), 2347 Quantity: match.Quantity, 2348 Rate: match.Rate, 2349 Address: order.ExtractAddress(match.Taker), 2350 ServerTime: stamp, 2351 FeeRateBase: match.FeeRateBase, 2352 FeeRateQuote: match.FeeRateQuote, 2353 Side: uint8(order.Maker), 2354 }, &msgjson.Match{ 2355 OrderID: idToBytes(match.Taker.ID()), 2356 MatchID: idToBytes(match.ID()), 2357 Quantity: match.Quantity, 2358 Rate: match.Rate, 2359 Address: makerAddr, 2360 ServerTime: stamp, 2361 FeeRateBase: match.FeeRateBase, 2362 FeeRateQuote: match.FeeRateQuote, 2363 Side: uint8(order.Taker), 2364 } 2365 } 2366 2367 // readMatches translates a slice of raw matches from the market manager into 2368 // a slice of matchTrackers. 2369 func readMatches(matchSets []*order.MatchSet) []*matchTracker { 2370 // The initial capacity guess here is a minimum, but will avoid a few 2371 // reallocs. 2372 nowMs := unixMsNow() 2373 matches := make([]*matchTracker, 0, len(matchSets)) 2374 for _, matchSet := range matchSets { 2375 for _, match := range matchSet.Matches() { 2376 maker := match.Maker 2377 base, quote := maker.BaseAsset, maker.QuoteAsset 2378 var makerSwapAsset, takerSwapAsset uint32 2379 if maker.Sell { 2380 makerSwapAsset = base 2381 takerSwapAsset = quote 2382 } else { 2383 makerSwapAsset = quote 2384 takerSwapAsset = base 2385 } 2386 2387 matches = append(matches, &matchTracker{ 2388 Match: match, 2389 time: nowMs, 2390 matchTime: match.Epoch.End(), 2391 makerStatus: &swapStatus{ 2392 swapAsset: makerSwapAsset, 2393 redeemAsset: takerSwapAsset, 2394 }, 2395 takerStatus: &swapStatus{ 2396 swapAsset: takerSwapAsset, 2397 redeemAsset: makerSwapAsset, 2398 }, 2399 }) 2400 } 2401 } 2402 return matches 2403 } 2404 2405 // Negotiate takes ownership of the matches and begins swap negotiation. For 2406 // reliable identification of completed orders when redeem acks are received and 2407 // processed by processAck, BeginMatchAndNegotiate should be called prior to 2408 // matching and order status/amount updates, and EndMatchAndNegotiate should be 2409 // called after Negotiate. This locking sequence allows for orders that may 2410 // already be involved in active swaps to remain unmodified by the 2411 // Matcher/Market until new matches are recorded by the Swapper in Negotiate. If 2412 // this is not done, it is possible that an order may be flagged as completed if 2413 // a swap A completes after Matching and creation of swap B but before Negotiate 2414 // has a chance to record the new swap. 2415 func (s *Swapper) Negotiate(matchSets []*order.MatchSet) { 2416 // If the Swapper is stopping, the Markets should also be stopping, but 2417 // block this just in case. 2418 s.handlerMtx.RLock() 2419 defer s.handlerMtx.RUnlock() 2420 if s.stop { 2421 log.Errorf("Negotiate called on stopped swapper. Matches lost!") 2422 return 2423 } 2424 2425 // Lock trade order coins, and get current optimal fee rates. Also filter 2426 // out matches with unsupported assets, which should not happen if the 2427 // Market is behaving, but be defensive. 2428 supportedMatchSets := matchSets[:0] // same buffer, start empty 2429 swapOrders := make([]order.Order, 0, 2*len(matchSets)) // size guess, with the single maker case 2430 for _, match := range matchSets { 2431 supportedMatchSets = append(supportedMatchSets, match) 2432 2433 if match.Taker.Type() == order.CancelOrderType { 2434 continue 2435 } 2436 2437 swapOrders = append(swapOrders, match.Taker) 2438 for _, maker := range match.Makers { 2439 swapOrders = append(swapOrders, maker) 2440 } 2441 } 2442 matchSets = supportedMatchSets 2443 2444 s.LockOrdersCoins(swapOrders) 2445 2446 // Set up the matchTrackers, which includes a slice of Matches. 2447 matches := readMatches(matchSets) 2448 2449 // Record the matches. If any DB updates fail, no swaps proceed. We could 2450 // let the others proceed, but that could seem selective trickery to the 2451 // clients. 2452 for _, match := range matches { 2453 // Note that matches where the taker order is a cancel will be stored 2454 // with status MatchComplete, and without the maker or taker swap 2455 // addresses. The match will also be flagged as inactive since there is 2456 // no associated swap negotiation. 2457 2458 // TODO: Initially store cancel matches lacking ack sigs as active, only 2459 // flagging as inactive when both maker and taker match ack sigs have 2460 // been received. The client will need a mechanism to provide the ack, 2461 // perhaps having the server resend missing match ack requests on client 2462 // connect. 2463 if err := s.storage.InsertMatch(match.Match); err != nil { 2464 log.Errorf("InsertMatch (match id=%v) failed: %v", match.ID(), err) 2465 // TODO: notify clients (notification or response to what?) 2466 // abortAll() 2467 return 2468 } 2469 } 2470 2471 userMatches := make(map[account.AccountID][]*messageAcker) 2472 // addUserMatch signs a match notification message, and add the data 2473 // required to process the acknowledgment to the userMatches map. 2474 addUserMatch := func(acker *messageAcker) { 2475 s.authMgr.Sign(acker.params) 2476 userMatches[acker.user] = append(userMatches[acker.user], acker) 2477 } 2478 2479 // Setting length to max possible, which is over-allocating by the number of 2480 // cancels. 2481 toMonitor := make([]*matchTracker, 0, len(matches)) 2482 for _, match := range matches { 2483 if match.Taker.Type() == order.CancelOrderType { 2484 // If this is a cancellation, there is nothing to track. Just cancel 2485 // the target order by removing it from the DB. It is already 2486 // removed from book by the Market. 2487 err := s.storage.CancelOrder(match.Maker) // TODO: do this in Market? 2488 if err != nil { 2489 log.Errorf("Failed to cancel order %v", match.Maker) 2490 // If the DB update failed, the target order status was not 2491 // updated, but removed from the in-memory book. This is 2492 // potentially a critical failure since the dex will restore the 2493 // book from the DB. TODO: Notify clients. 2494 return 2495 } 2496 } else { 2497 toMonitor = append(toMonitor, match) 2498 } 2499 2500 // Create an acker for maker and taker, sharing the same matchTracker. 2501 makerMsg, takerMsg := matchNotifications(match) // msgjson.Match for each party 2502 addUserMatch(&messageAcker{ 2503 user: match.Maker.User(), 2504 match: match, 2505 params: makerMsg, 2506 isMaker: true, 2507 // isAudit: false, 2508 }) 2509 addUserMatch(&messageAcker{ 2510 user: match.Taker.User(), 2511 match: match, 2512 params: takerMsg, 2513 isMaker: false, 2514 // isAudit: false, 2515 }) 2516 } 2517 2518 // Add the matches to the matches/userMatches maps. 2519 s.matchMtx.Lock() 2520 for _, match := range toMonitor { 2521 s.addMatch(match) 2522 } 2523 s.matchMtx.Unlock() 2524 2525 // Send the user match notifications. 2526 for user, matches := range userMatches { 2527 // msgs is a slice of msgjson.Match created by newMatchAckers 2528 // (matchNotifications) for all makers and takers. 2529 msgs := make([]msgjson.Signable, 0, len(matches)) 2530 for _, m := range matches { 2531 msgs = append(msgs, m.params) 2532 } 2533 2534 // Solicit match acknowledgments. Each Match is signed in addUserMatch. 2535 req, err := msgjson.NewRequest(comms.NextID(), msgjson.MatchRoute, msgs) 2536 if err != nil { 2537 log.Errorf("error creating match notification request: %v", err) 2538 // Should never happen, but the client can still use match_status. 2539 continue 2540 } 2541 2542 // Copy the loop variables for capture by the match acknowledgement 2543 // response handler. 2544 u, m := user, matches 2545 log.Debugf("Negotiate: sending 'match' ack request to user %v for %d matches", 2546 u, len(m)) 2547 2548 // Send the request. 2549 err = s.authMgr.Request(u, req, func(_ comms.Link, resp *msgjson.Message) { 2550 s.processMatchAcks(u, resp, m) 2551 }) 2552 if err != nil { 2553 log.Infof("Failed to send %v request to %v. The match will be returned in the connect response.", 2554 req.Route, u) 2555 } 2556 } 2557 } 2558 2559 func idToBytes(id [order.OrderIDSize]byte) []byte { 2560 return id[:] 2561 }