decred.org/dcrdex@v1.0.5/server/auth/auth.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 auth 5 6 import ( 7 "bytes" 8 "context" 9 "crypto/sha256" 10 "encoding/hex" 11 "fmt" 12 "math" 13 "sync" 14 "time" 15 16 "decred.org/dcrdex/dex" 17 "decred.org/dcrdex/dex/encode" 18 "decred.org/dcrdex/dex/msgjson" 19 "decred.org/dcrdex/dex/order" 20 "decred.org/dcrdex/dex/wait" 21 "decred.org/dcrdex/server/account" 22 "decred.org/dcrdex/server/asset" 23 "decred.org/dcrdex/server/comms" 24 "decred.org/dcrdex/server/db" 25 26 "github.com/decred/dcrd/dcrec/secp256k1/v4" 27 "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" 28 ) 29 30 const ( 31 cancelThreshWindow = 100 // spec 32 ScoringMatchLimit = 60 // last N matches (success or at-fault fail) to be considered in swap inaction scoring 33 scoringOrderLimit = 40 // last N orders to be considered in preimage miss scoring 34 35 maxIDsPerOrderStatusRequest = 10_000 36 ) 37 38 var ( 39 ErrUserNotConnected = dex.ErrorKind("user not connected") 40 ) 41 42 func unixMsNow() time.Time { 43 return time.Now().Truncate(time.Millisecond).UTC() 44 } 45 46 // Storage updates and fetches account-related data from what is presumably a 47 // database. 48 type Storage interface { 49 // Account retrieves account info for the specified account ID and lock time 50 // threshold, which determines when a bond is considered expired. 51 Account(account.AccountID, time.Time) (acct *account.Account, bonds []*db.Bond) 52 53 CreateAccountWithBond(acct *account.Account, bond *db.Bond) error 54 AddBond(acct account.AccountID, bond *db.Bond) error 55 DeleteBond(assetID uint32, coinID []byte) error 56 FetchPrepaidBond(bondCoinID []byte) (strength uint32, lockTime int64, err error) 57 DeletePrepaidBond(coinID []byte) error 58 StorePrepaidBonds(coinIDs [][]byte, strength uint32, lockTime int64) error 59 60 AccountInfo(aid account.AccountID) (*db.Account, error) 61 62 UserOrderStatuses(aid account.AccountID, base, quote uint32, oids []order.OrderID) ([]*db.OrderStatus, error) 63 ActiveUserOrderStatuses(aid account.AccountID) ([]*db.OrderStatus, error) 64 CompletedUserOrders(aid account.AccountID, N int) (oids []order.OrderID, compTimes []int64, err error) 65 ExecutedCancelsForUser(aid account.AccountID, N int) ([]*db.CancelRecord, error) 66 CompletedAndAtFaultMatchStats(aid account.AccountID, lastN int) ([]*db.MatchOutcome, error) 67 UserMatchFails(aid account.AccountID, lastN int) ([]*db.MatchFail, error) 68 ForgiveMatchFail(mid order.MatchID) (bool, error) 69 PreimageStats(user account.AccountID, lastN int) ([]*db.PreimageResult, error) 70 AllActiveUserMatches(aid account.AccountID) ([]*db.MatchData, error) 71 MatchStatuses(aid account.AccountID, base, quote uint32, matchIDs []order.MatchID) ([]*db.MatchStatus, error) 72 } 73 74 // Signer signs messages. The message must be a 32-byte hash. 75 type Signer interface { 76 Sign(hash []byte) *ecdsa.Signature 77 PubKey() *secp256k1.PublicKey 78 } 79 80 // FeeChecker is a function for retrieving the details for a fee payment txn. 81 type FeeChecker func(assetID uint32, coinID []byte) (addr string, val uint64, confs int64, err error) 82 83 // BondCoinChecker is a function for locating an unspent bond, and extracting 84 // the amount, lockTime, and account ID. The confirmations of the bond 85 // transaction are also provided. 86 type BondCoinChecker func(ctx context.Context, assetID uint32, ver uint16, 87 coinID []byte) (amt, lockTime, confs int64, acct account.AccountID, err error) 88 89 // BondTxParser parses a dex fidelity bond transaction and the redeem script of 90 // the first output of the transaction, which must be the actual bond output. 91 // The returned account ID is from the second output. This will become a 92 // multi-asset checker. 93 // 94 // NOTE: For DCR, and possibly all assets, the bond script is reconstructed from 95 // the null data output, and it is verified that the bond output pays to this 96 // script. As such, there is no provided bondData (redeem script for UTXO 97 // assets), but this may need for other assets. 98 type BondTxParser func(assetID uint32, ver uint16, rawTx []byte) (bondCoinID []byte, 99 amt int64, lockTime int64, acct account.AccountID, err error) 100 101 // TxDataSource retrieves the raw transaction for a coin ID. 102 type TxDataSource func(coinID []byte) (rawTx []byte, err error) 103 104 // A respHandler is the handler for the response to a DEX-originating request. A 105 // respHandler has a time associated with it so that old unused handlers can be 106 // detected and deleted. 107 type respHandler struct { 108 f func(comms.Link, *msgjson.Message) 109 expire *time.Timer 110 } 111 112 // clientInfo represents a DEX client, including account information and last 113 // known comms.Link. 114 type clientInfo struct { 115 acct *account.Account 116 conn comms.Link 117 118 mtx sync.Mutex 119 respHandlers map[uint64]*respHandler 120 tier int64 121 score int32 122 bonds []*db.Bond // only confirmed and active, not pending 123 } 124 125 // not thread-safe 126 func (client *clientInfo) bondTier() (bondTier int64) { 127 for _, bi := range client.bonds { 128 bondTier += int64(bi.Strength) 129 } 130 return 131 } 132 133 // not thread-safe 134 func (client *clientInfo) addBond(bond *db.Bond) (bondTier int64) { 135 var dup bool 136 for _, bi := range client.bonds { 137 bondTier += int64(bi.Strength) 138 dup = dup || (bi.AssetID == bond.AssetID && bytes.Equal(bi.CoinID, bond.CoinID)) 139 } 140 141 if !dup { // idempotent 142 client.bonds = append(client.bonds, bond) 143 bondTier += int64(bond.Strength) 144 } 145 146 return 147 } 148 149 // not thread-safe 150 func (client *clientInfo) pruneBonds(lockTimeThresh int64) (pruned []*db.Bond, bondTier int64) { 151 if len(client.bonds) == 0 { 152 return 153 } 154 155 var n int 156 for _, bond := range client.bonds { 157 if bond.LockTime >= lockTimeThresh { // not expired 158 if len(pruned) > 0 /* n < i */ { // a prior bond was removed, must move this element up in the slice 159 client.bonds[n] = bond 160 } 161 n++ 162 bondTier += int64(bond.Strength) 163 continue 164 } 165 log.Infof("Expiring user %v bond %v (%s)", client.acct.ID, 166 coinIDString(bond.AssetID, bond.CoinID), dex.BipIDSymbol(bond.AssetID)) 167 pruned = append(pruned, bond) 168 // n not incremented, next live bond shifts up 169 } 170 client.bonds = client.bonds[:n] // no-op if none expired 171 172 return 173 } 174 175 func (client *clientInfo) rmHandler(id uint64) bool { 176 client.mtx.Lock() 177 defer client.mtx.Unlock() 178 _, found := client.respHandlers[id] 179 if found { 180 delete(client.respHandlers, id) 181 } 182 return found 183 } 184 185 // logReq associates the specified response handler with the message ID. 186 func (client *clientInfo) logReq(id uint64, f func(comms.Link, *msgjson.Message), expireTime time.Duration, expire func()) { 187 client.mtx.Lock() 188 defer client.mtx.Unlock() 189 doExpire := func() { 190 // Delete the response handler, and call the provided expire function if 191 // (*clientInfo).respHandler has not already retrieved the handler 192 // function for execution. 193 if client.rmHandler(id) { 194 expire() 195 } 196 } 197 client.respHandlers[id] = &respHandler{ 198 f: f, 199 expire: time.AfterFunc(expireTime, doExpire), 200 } 201 } 202 203 // respHandler extracts the response handler from the respHandlers map. If the 204 // handler is found, it is also deleted from the map before being returned, and 205 // the expiration Timer is stopped. 206 func (client *clientInfo) respHandler(id uint64) *respHandler { 207 client.mtx.Lock() 208 defer client.mtx.Unlock() 209 210 handler := client.respHandlers[id] 211 if handler == nil { 212 return nil 213 } 214 215 // Stop the expiration Timer. If the Timer fired after respHandler was 216 // called, but we found the response handler in the map, clientInfo.expire 217 // is waiting for the lock and will return false, thus preventing the 218 // registered expire func from executing. 219 handler.expire.Stop() 220 delete(client.respHandlers, id) 221 return handler 222 } 223 224 // AuthManager handles authentication-related tasks, including validating client 225 // signatures, maintaining association between accounts and `comms.Link`s, and 226 // signing messages with the DEX's private key. AuthManager manages requests to 227 // the 'connect' route. 228 type AuthManager struct { 229 wg sync.WaitGroup 230 storage Storage 231 signer Signer 232 parseBondTx BondTxParser 233 checkBond BondCoinChecker // fidelity bond amount, lockTime, acct, and confs 234 miaUserTimeout time.Duration 235 unbookFun func(account.AccountID) 236 route func(route string, handler comms.MsgHandler) 237 238 bondExpiry time.Duration // a bond is expired when time.Until(lockTime) < bondExpiry 239 bondAssets map[uint32]*msgjson.BondAsset 240 241 freeCancels bool 242 penaltyThreshold int32 243 cancelThresh float64 244 245 // latencyQ is a queue for fee coin waiters to deal with latency. 246 latencyQ *wait.TickerQueue 247 248 bondWaiterMtx sync.Mutex 249 bondWaiterIdx map[string]struct{} 250 251 connMtx sync.RWMutex 252 users map[account.AccountID]*clientInfo 253 conns map[uint64]*clientInfo 254 unbookers map[account.AccountID]*time.Timer 255 256 violationMtx sync.Mutex 257 matchOutcomes map[account.AccountID]*latestMatchOutcomes 258 preimgOutcomes map[account.AccountID]*latestPreimageOutcomes 259 orderOutcomes map[account.AccountID]*latestOrders // cancel/complete, was in clientInfo.recentOrders 260 261 txDataSources map[uint32]TxDataSource 262 263 prepaidBondMtx sync.Mutex 264 } 265 266 // violation badness 267 const ( 268 // preimage miss 269 preimageMissScore = -2 // book spoof, no match, no stuck funds 270 271 // failure to act violations 272 noSwapAsMakerScore = -4 // book spoof, match with taker order affected, no stuck funds 273 noSwapAsTakerScore = -11 // maker has contract stuck for 20 hrs 274 noRedeemAsMakerScore = -7 // taker has contract stuck for 8 hrs 275 noRedeemAsTakerScore = -1 // just dumb, counterparty not inconvenienced 276 277 // cancel rate exceeds threshold 278 excessiveCancels = -5 279 280 successScore = 1 // offsets the violations 281 282 DefaultPenaltyThreshold = 20 283 ) 284 285 // Violation represents a specific infraction. For example, not broadcasting a 286 // swap contract transaction by the deadline as the maker. 287 type Violation int32 288 289 const ( 290 ViolationInvalid Violation = iota - 2 291 ViolationForgiven 292 ViolationSwapSuccess 293 ViolationPreimageMiss 294 ViolationNoSwapAsMaker 295 ViolationNoSwapAsTaker 296 ViolationNoRedeemAsMaker 297 ViolationNoRedeemAsTaker 298 ViolationCancelRate 299 ) 300 301 var violations = map[Violation]struct { 302 score int32 303 desc string 304 }{ 305 ViolationSwapSuccess: {successScore, "swap success"}, 306 ViolationForgiven: {1, "forgiveness"}, 307 ViolationPreimageMiss: {preimageMissScore, "preimage miss"}, 308 ViolationNoSwapAsMaker: {noSwapAsMakerScore, "no swap as maker"}, 309 ViolationNoSwapAsTaker: {noSwapAsTakerScore, "no swap as taker"}, 310 ViolationNoRedeemAsMaker: {noRedeemAsMakerScore, "no redeem as maker"}, 311 ViolationNoRedeemAsTaker: {noRedeemAsTakerScore, "no redeem as taker"}, 312 ViolationCancelRate: {excessiveCancels, "excessive cancels"}, 313 ViolationInvalid: {0, "invalid violation"}, 314 } 315 316 // Score returns the Violation's score, which is a representation of the 317 // relative severity of the infraction. 318 func (v Violation) Score() int32 { 319 return violations[v].score 320 } 321 322 // String returns a description of the Violation. 323 func (v Violation) String() string { 324 return violations[v].desc 325 } 326 327 // NoActionStep is the action that the user failed to take. This is used to 328 // define valid inputs to the Inaction method. 329 type NoActionStep uint8 330 331 const ( 332 SwapSuccess NoActionStep = iota // success included for accounting purposes 333 NoSwapAsMaker 334 NoSwapAsTaker 335 NoRedeemAsMaker 336 NoRedeemAsTaker 337 ) 338 339 // Violation returns the corresponding Violation for the misstep represented by 340 // the NoActionStep. 341 func (step NoActionStep) Violation() Violation { 342 switch step { 343 case SwapSuccess: 344 return ViolationSwapSuccess 345 case NoSwapAsMaker: 346 return ViolationNoSwapAsMaker 347 case NoSwapAsTaker: 348 return ViolationNoSwapAsTaker 349 case NoRedeemAsMaker: 350 return ViolationNoRedeemAsMaker 351 case NoRedeemAsTaker: 352 return ViolationNoRedeemAsTaker 353 default: 354 return ViolationInvalid 355 } 356 } 357 358 // String returns the description of the NoActionStep's corresponding Violation. 359 func (step NoActionStep) String() string { 360 return step.Violation().String() 361 } 362 363 // Config is the configuration settings for the AuthManager, and the only 364 // argument to its constructor. 365 type Config struct { 366 // Storage is an interface for storing and retrieving account-related info. 367 Storage Storage 368 // Signer is an interface that signs messages. In practice, Signer is 369 // satisfied by a secp256k1.PrivateKey. 370 Signer Signer 371 372 Route func(route string, handler comms.MsgHandler) 373 374 // BondExpiry is the time in seconds left until a bond's LockTime is reached 375 // that defines when a bond is considered expired. 376 BondExpiry uint64 377 // BondAssets indicates the supported bond assets and parameters. 378 BondAssets map[string]*msgjson.BondAsset 379 // BondTxParser performs rudimentary validation of a raw time-locked 380 // fidelity bond transaction. e.g. dcr.ParseBondTx 381 BondTxParser BondTxParser 382 // BondChecker locates an unspent bond, and extracts the amount, lockTime, 383 // and account ID, plus txn confirmations. 384 BondChecker BondCoinChecker 385 386 // TxDataSources are sources of tx data for a coin ID. 387 TxDataSources map[uint32]TxDataSource 388 389 // UserUnbooker is a function for unbooking all of a user's orders. 390 UserUnbooker func(account.AccountID) 391 // MiaUserTimeout is how long after a user disconnects until UserUnbooker is 392 // called for that user. 393 MiaUserTimeout time.Duration 394 395 CancelThreshold float64 396 FreeCancels bool 397 398 // PenaltyThreshold defines the score deficit at which a user's bond is 399 // revoked. 400 PenaltyThreshold uint32 401 } 402 403 // NewAuthManager is the constructor for an AuthManager. 404 func NewAuthManager(cfg *Config) *AuthManager { 405 // A penalty threshold of 0 is not sensible, so have a default. 406 penaltyThreshold := int32(cfg.PenaltyThreshold) 407 if penaltyThreshold <= 0 { 408 penaltyThreshold = DefaultPenaltyThreshold 409 } 410 // Invert sign for internal use. 411 if penaltyThreshold > 0 { 412 penaltyThreshold *= -1 413 } 414 // Re-key the maps for efficiency in AuthManager methods. 415 bondAssets := make(map[uint32]*msgjson.BondAsset, len(cfg.BondAssets)) 416 for _, asset := range cfg.BondAssets { 417 bondAssets[asset.ID] = asset 418 } 419 420 auth := &AuthManager{ 421 storage: cfg.Storage, 422 signer: cfg.Signer, 423 bondAssets: bondAssets, 424 bondExpiry: time.Duration(cfg.BondExpiry) * time.Second, 425 parseBondTx: cfg.BondTxParser, // e.g. dcr's ParseBondTx 426 checkBond: cfg.BondChecker, // e.g. dcr's BondCoin 427 miaUserTimeout: cfg.MiaUserTimeout, 428 unbookFun: cfg.UserUnbooker, 429 route: cfg.Route, 430 freeCancels: cfg.FreeCancels, 431 penaltyThreshold: penaltyThreshold, 432 cancelThresh: cfg.CancelThreshold, 433 latencyQ: wait.NewTickerQueue(recheckInterval), 434 users: make(map[account.AccountID]*clientInfo), 435 conns: make(map[uint64]*clientInfo), 436 unbookers: make(map[account.AccountID]*time.Timer), 437 bondWaiterIdx: make(map[string]struct{}), 438 matchOutcomes: make(map[account.AccountID]*latestMatchOutcomes), 439 preimgOutcomes: make(map[account.AccountID]*latestPreimageOutcomes), 440 orderOutcomes: make(map[account.AccountID]*latestOrders), 441 txDataSources: cfg.TxDataSources, 442 } 443 444 // Unauthenticated 445 cfg.Route(msgjson.ConnectRoute, auth.handleConnect) 446 cfg.Route(msgjson.PostBondRoute, auth.handlePostBond) 447 cfg.Route(msgjson.PreValidateBondRoute, auth.handlePreValidateBond) 448 cfg.Route(msgjson.MatchStatusRoute, auth.handleMatchStatus) 449 cfg.Route(msgjson.OrderStatusRoute, auth.handleOrderStatus) 450 return auth 451 } 452 453 func (auth *AuthManager) unbookUserOrders(user account.AccountID) { 454 log.Tracef("Unbooking all orders for user %v", user) 455 auth.unbookFun(user) 456 auth.connMtx.Lock() 457 delete(auth.unbookers, user) 458 auth.connMtx.Unlock() 459 } 460 461 // ExpectUsers specifies which users are expected to connect within a certain 462 // time or have their orders unbooked (revoked). This should be run prior to 463 // starting the AuthManager. This is not part of the constructor since it is 464 // convenient to obtain this information from the Market's Books, and Market 465 // requires the AuthManager. The same information could be pulled from storage, 466 // but the Market is the authoritative book. The AuthManager should be started 467 // via Run immediately after calling ExpectUsers so the users can connect. 468 func (auth *AuthManager) ExpectUsers(users map[account.AccountID]struct{}, within time.Duration) { 469 log.Debugf("Expecting %d users with booked orders to connect within %v", len(users), within) 470 for user := range users { 471 user := user // bad go 472 auth.unbookers[user] = time.AfterFunc(within, func() { auth.unbookUserOrders(user) }) 473 } 474 } 475 476 // GraceLimit returns the number of initial orders allowed for a new user before 477 // the cancellation rate threshold is enforced. 478 func (auth *AuthManager) GraceLimit() int { 479 // Grace period if: total/(1+total) <= thresh OR total <= thresh/(1-thresh). 480 return int(math.Round(1e8*auth.cancelThresh/(1-auth.cancelThresh))) / 1e8 481 } 482 483 // RecordCancel records a user's executed cancel order, including the canceled 484 // order ID, and the time when the cancel was executed. 485 func (auth *AuthManager) RecordCancel(user account.AccountID, oid, target order.OrderID, epochGap int32, t time.Time) { 486 score := auth.recordOrderDone(user, oid, &target, epochGap, t.UnixMilli()) 487 488 rep, tierChanged, scoreChanged := auth.computeUserReputation(user, score) 489 effectiveTier := rep.EffectiveTier() 490 log.Debugf("RecordCancel: user %v strikes %d, bond tier %v => trading tier %v", 491 user, score, rep.BondedTier, effectiveTier) 492 // If their tier sinks below 1, unbook their orders and send a note. 493 if tierChanged && effectiveTier < 1 { 494 details := fmt.Sprintf("excessive cancellation rate, new tier = %d", effectiveTier) 495 auth.Penalize(user, account.CancellationRate, details) 496 } 497 if tierChanged { 498 go auth.sendTierChanged(user, rep, "excessive, cancellation rate") 499 } else if scoreChanged { 500 go auth.sendScoreChanged(user, rep) 501 } 502 503 } 504 505 // RecordCompletedOrder records a user's completed order, where completed means 506 // a swap involving the order was successfully completed and the order is no 507 // longer on the books if it ever was. 508 func (auth *AuthManager) RecordCompletedOrder(user account.AccountID, oid order.OrderID, t time.Time) { 509 score := auth.recordOrderDone(user, oid, nil, db.EpochGapNA, t.UnixMilli()) 510 rep, tierChanged, scoreChanged := auth.computeUserReputation(user, score) // may raise tier 511 if tierChanged { 512 log.Tracef("RecordCompletedOrder: tier changed for user %v strikes %d, bond tier %v => trading tier %v", 513 user, score, rep.BondedTier, rep.EffectiveTier()) 514 go auth.sendTierChanged(user, rep, "successful order completion") 515 } else if scoreChanged { 516 go auth.sendScoreChanged(user, rep) 517 } 518 } 519 520 // recordOrderDone records that an order has finished processing. This can be a 521 // cancel order, which matched and unbooked another order, or a trade order that 522 // completed the swap negotiation. Note that in the case of a cancel, oid refers 523 // to the ID of the cancel order itself, while target is non-nil for cancel 524 // orders. The user's new score is returned, which can be used to compute the 525 // user's tier with computeUserTier. 526 func (auth *AuthManager) recordOrderDone(user account.AccountID, oid order.OrderID, target *order.OrderID, epochGap int32, tMS int64) (score int32) { 527 auth.violationMtx.Lock() 528 if orderOutcomes, found := auth.orderOutcomes[user]; found { 529 orderOutcomes.add(&oidStamped{ 530 OrderID: oid, 531 time: tMS, 532 target: target, 533 epochGap: epochGap, 534 }) 535 score = auth.userScore(user) 536 auth.violationMtx.Unlock() 537 log.Debugf("Recorded order %v that has finished processing: user=%v, time=%v, target=%v", 538 oid, user, tMS, target) 539 return 540 } 541 auth.violationMtx.Unlock() 542 543 // The user is currently not connected and authenticated. When the user logs 544 // back in, their history will be reloaded (loadUserScore) and their tier 545 // recomputed, but compute their score now from DB for the caller. 546 var err error 547 score, err = auth.loadUserScore(user) 548 if err != nil { 549 log.Errorf("Failed to load order and match outcomes for user %v: %v", user, err) 550 return 0 551 } 552 553 return 554 } 555 556 // Run runs the AuthManager until the context is canceled. Satisfies the 557 // dex.Runner interface. 558 func (auth *AuthManager) Run(ctx context.Context) { 559 auth.wg.Add(1) 560 go func() { 561 defer auth.wg.Done() 562 t := time.NewTicker(20 * time.Second) 563 defer t.Stop() 564 565 for { 566 select { 567 case <-t.C: 568 auth.checkBonds() 569 case <-ctx.Done(): 570 return 571 } 572 } 573 }() 574 575 auth.wg.Add(1) 576 go func() { 577 defer auth.wg.Done() 578 auth.latencyQ.Run(ctx) 579 }() 580 581 <-ctx.Done() 582 auth.connMtx.Lock() 583 defer auth.connMtx.Unlock() 584 for user, ub := range auth.unbookers { 585 ub.Stop() 586 delete(auth.unbookers, user) 587 } 588 589 // Wait for latencyQ and checkBonds. 590 auth.wg.Wait() 591 // TODO: wait for running comms route handlers and other DB writers. 592 } 593 594 // Route wraps the comms.Route function, storing the response handler with the 595 // associated clientInfo, and sending the message on the current comms.Link for 596 // the client. 597 func (auth *AuthManager) Route(route string, handler func(account.AccountID, *msgjson.Message) *msgjson.Error) { 598 auth.route(route, func(conn comms.Link, msg *msgjson.Message) *msgjson.Error { 599 client := auth.conn(conn) 600 if client == nil { 601 return &msgjson.Error{ 602 Code: msgjson.UnauthorizedConnection, 603 Message: "cannot use route '" + route + "' on an unauthorized connection", 604 } 605 } 606 msgErr := handler(client.acct.ID, msg) 607 if msgErr != nil { 608 log.Debugf("Handling of '%s' request for user %v failed: %v", route, client.acct.ID, msgErr) 609 } 610 return msgErr 611 }) 612 } 613 614 // Message signing and signature verification. 615 616 // checkSigS256 checks that the message's signature was created with the 617 // private key for the provided secp256k1 public key. 618 func checkSigS256(msg, sig []byte, pubKey *secp256k1.PublicKey) error { 619 signature, err := ecdsa.ParseDERSignature(sig) 620 if err != nil { 621 return fmt.Errorf("error decoding secp256k1 Signature from bytes: %w", err) 622 } 623 hash := sha256.Sum256(msg) 624 if !signature.Verify(hash[:], pubKey) { 625 return fmt.Errorf("secp256k1 signature verification failed") 626 } 627 return nil 628 } 629 630 // Auth validates the signature/message pair with the users public key. 631 func (auth *AuthManager) Auth(user account.AccountID, msg, sig []byte) error { 632 client := auth.user(user) 633 if client == nil { 634 return dex.NewError(ErrUserNotConnected, user.String()) 635 } 636 return checkSigS256(msg, sig, client.acct.PubKey) 637 } 638 639 // SignMsg signs the message with the DEX private key, returning the DER encoded 640 // signature. SHA256 is used to hash the message before signing it. 641 func (auth *AuthManager) SignMsg(msg []byte) []byte { 642 hash := sha256.Sum256(msg) 643 return auth.signer.Sign(hash[:]).Serialize() 644 } 645 646 // Sign signs the msgjson.Signables with the DEX private key. 647 func (auth *AuthManager) Sign(signables ...msgjson.Signable) { 648 for _, signable := range signables { 649 sig := auth.SignMsg(signable.Serialize()) 650 signable.SetSig(sig) 651 } 652 } 653 654 // Response and notification (non-request) messages 655 656 // Send sends the non-Request-type msgjson.Message to the client identified by 657 // the specified account ID. The message is sent asynchronously, so an error is 658 // only generated if the specified user is not connected and authorized, if the 659 // message fails marshalling, or if the link is in a failing state. See 660 // dex/ws.(*WSLink).Send for more information. 661 func (auth *AuthManager) Send(user account.AccountID, msg *msgjson.Message) error { 662 client := auth.user(user) 663 if client == nil { 664 log.Debugf("Send requested for disconnected user %v", user) 665 return dex.NewError(ErrUserNotConnected, user.String()) 666 } 667 668 err := client.conn.Send(msg) 669 if err != nil { 670 log.Debugf("error sending on link: %v", err) 671 // Remove client assuming connection is broken, requiring reconnect. 672 auth.removeClient(client) 673 // client.conn.Disconnect() // async removal 674 } 675 return err 676 } 677 678 // Notify sends a message to a client. The message should be a notification. 679 // See msgjson.NewNotification. 680 func (auth *AuthManager) Notify(acctID account.AccountID, msg *msgjson.Message) { 681 if err := auth.Send(acctID, msg); err != nil { 682 log.Infof("Failed to send notification to user %s: %v", acctID, err) 683 } 684 } 685 686 // Requests 687 688 // DefaultRequestTimeout is the default timeout for requests to wait for 689 // responses from connected users after the request is successfully sent. 690 const DefaultRequestTimeout = 30 * time.Second 691 692 func (auth *AuthManager) request(user account.AccountID, msg *msgjson.Message, f func(comms.Link, *msgjson.Message), 693 expireTimeout time.Duration, expire func()) error { 694 695 client := auth.user(user) 696 if client == nil { 697 log.Debugf("Send requested for disconnected user %v", user) 698 return dex.NewError(ErrUserNotConnected, user.String()) 699 } 700 // log.Tracef("Registering '%s' request ID %d for user %v (auth clientInfo)", msg.Route, msg.ID, user) 701 client.logReq(msg.ID, f, expireTimeout, expire) 702 // auth.handleResponse checks clientInfo map and the found client's request 703 // handler map, where the expire function should be found for msg.ID. 704 err := client.conn.Request(msg, auth.handleResponse, expireTimeout, func() {}) 705 if err != nil { 706 log.Debugf("error sending request ID %d: %v", msg.ID, err) 707 // Remove the responseHandler registered by logReq and stop the expire 708 // timer so that it does not eventually fire and run the expire func. 709 // The caller receives a non-nil error to deal with it. 710 client.respHandler(msg.ID) // drop the removed handler 711 // Remove client assuming connection is broken, requiring reconnect. 712 auth.removeClient(client) 713 // client.conn.Disconnect() // async removal 714 } 715 return err 716 } 717 718 // Request sends the Request-type msgjson.Message to the client identified by 719 // the specified account ID. The user must respond within DefaultRequestTimeout 720 // of the request. Late responses are not handled. 721 func (auth *AuthManager) Request(user account.AccountID, msg *msgjson.Message, f func(comms.Link, *msgjson.Message)) error { 722 return auth.request(user, msg, f, DefaultRequestTimeout, func() {}) 723 } 724 725 // RequestWithTimeout sends the Request-type msgjson.Message to the client 726 // identified by the specified account ID. If the user responds within 727 // expireTime of the request, the response handler is called, otherwise the 728 // expire function is called. If the response handler is called, it is 729 // guaranteed that the request Message.ID is equal to the response Message.ID 730 // (see handleResponse). 731 func (auth *AuthManager) RequestWithTimeout(user account.AccountID, msg *msgjson.Message, f func(comms.Link, *msgjson.Message), 732 expireTimeout time.Duration, expire func()) error { 733 return auth.request(user, msg, f, expireTimeout, expire) 734 } 735 736 const ( 737 // These coefficients are used to compute a user's swap limit adjustment via 738 // UserOrderLimitAdjustment based on the cumulative amounts in the different 739 // match outcomes. 740 successWeight int64 = 3 741 stuckLongWeight int64 = -5 742 stuckShortWeight int64 = -3 743 spoofedWeight int64 = -1 744 ) 745 746 func (auth *AuthManager) integrateOutcomes( 747 matchOutcomes *latestMatchOutcomes, 748 preimgOutcomes *latestPreimageOutcomes, 749 orderOutcomes *latestOrders, 750 ) (score, successCount, piMissCount int32) { 751 752 if matchOutcomes != nil { 753 matchCounts := matchOutcomes.binViolations() 754 for v, count := range matchCounts { 755 score += v.Score() * int32(count) 756 } 757 successCount = int32(matchCounts[ViolationSwapSuccess]) 758 } 759 if preimgOutcomes != nil { 760 piMissCount = preimgOutcomes.misses() 761 score += ViolationPreimageMiss.Score() * piMissCount 762 } 763 if !auth.freeCancels { 764 totalOrds, cancels := orderOutcomes.counts() // completions := totalOrds - cancels 765 if totalOrds > auth.GraceLimit() { 766 cancelRate := float64(cancels) / float64(totalOrds) 767 if cancelRate > auth.cancelThresh { 768 score += ViolationCancelRate.Score() 769 } 770 } 771 } 772 return 773 } 774 775 // userScore computes an authenticated user's score from their recent order and 776 // match outcomes. They must have entries in the outcome maps. Use loadUserScore 777 // to compute score from history in DB. This must be called with the 778 // violationMtx locked. 779 func (auth *AuthManager) userScore(user account.AccountID) (score int32) { 780 score, _, _ = auth.integrateOutcomes(auth.matchOutcomes[user], auth.preimgOutcomes[user], auth.orderOutcomes[user]) 781 return score 782 } 783 784 // UserScore calculates the user's score, loading it from storage if necessary. 785 func (auth *AuthManager) UserScore(user account.AccountID) (score int32, err error) { 786 auth.violationMtx.Lock() 787 if _, found := auth.matchOutcomes[user]; found { 788 score = auth.userScore(user) 789 auth.violationMtx.Unlock() 790 return 791 } 792 auth.violationMtx.Unlock() 793 794 // The user is currently not connected and authenticated. When the user logs 795 // back in, their history will be reloaded (loadUserScore) and their tier 796 // recomputed, but compute their score now from DB for the caller. 797 score, err = auth.loadUserScore(user) 798 if err != nil { 799 return 0, fmt.Errorf("failed to load order and match outcomes for user %v: %v", user, err) 800 } 801 return 802 } 803 804 // UserReputation calculates some quantities related to the user's reputation. 805 // UserReputation satisfies market.AuthManager. 806 func (auth *AuthManager) UserReputation(user account.AccountID) (tier int64, score, maxScore int32, err error) { 807 maxScore = ScoringMatchLimit 808 score, err = auth.UserScore(user) 809 if err != nil { 810 return 811 } 812 r, _, _ := auth.computeUserReputation(user, score) 813 if r != nil { 814 return r.EffectiveTier(), r.Score, ScoringMatchLimit, nil 815 816 } 817 return 818 } 819 820 // userReputation computes the breakdown of a user's tier and score. 821 func (auth *AuthManager) userReputation(bondTier int64, score int32) *account.Reputation { 822 var penalties int32 823 if score < 0 { 824 penalties = score / auth.penaltyThreshold 825 } 826 return &account.Reputation{ 827 BondedTier: bondTier, 828 Penalties: uint16(penalties), 829 Score: score, 830 } 831 } 832 833 // tier computes a user's tier from their conduct score and bond tier. 834 func (auth *AuthManager) tier(bondTier int64, score int32) int64 { 835 return auth.userReputation(bondTier, score).EffectiveTier() 836 } 837 838 // computeUserReputation computes the user's tier given the provided score 839 // weighed against known active bonds. Note that bondTier is not a specific 840 // asset, and is just for logging, and it may be removed or changed to a map by 841 // asset ID. For online users, this will also indicate if the tier changed; this 842 // will always return false for offline users. 843 func (auth *AuthManager) computeUserReputation(user account.AccountID, score int32) (r *account.Reputation, tierChanged, scoreChanged bool) { 844 client := auth.user(user) 845 if client == nil { 846 // Offline. Load active bonds and legacyFeePaid flag from DB. 847 lockTimeThresh := time.Now().Add(auth.bondExpiry) 848 _, bonds := auth.storage.Account(user, lockTimeThresh) 849 var bondTier int64 850 for _, bond := range bonds { 851 bondTier += int64(bond.Strength) 852 } 853 return auth.userReputation(bondTier, score), false, false 854 } 855 856 client.mtx.Lock() 857 defer client.mtx.Unlock() 858 wasTier := client.tier 859 wasScore := client.score 860 bondTier := client.bondTier() 861 r = auth.userReputation(bondTier, score) 862 client.tier = r.EffectiveTier() 863 client.score = score 864 scoreChanged = wasScore != score 865 tierChanged = wasTier != client.tier 866 867 return 868 } 869 870 // ComputeUserTier computes the user's tier from their active bonds and conduct 871 // score. The bondTier is also returned. The DB is always consulted for 872 // computing the conduct score. Summing bond amounts may access the DB if the 873 // user is not presently connected. The tier for an unknown user is -1. 874 func (auth *AuthManager) ComputeUserReputation(user account.AccountID) *account.Reputation { 875 score, err := auth.loadUserScore(user) 876 if err != nil { 877 log.Errorf("failed to load user score: %v", err) 878 return nil 879 } 880 r, _, _ := auth.computeUserReputation(user, score) 881 return r 882 } 883 884 func (auth *AuthManager) registerMatchOutcome(user account.AccountID, misstep NoActionStep, mmid db.MarketMatchID, value uint64, refTime time.Time) (score int32) { 885 violation := misstep.Violation() 886 887 auth.violationMtx.Lock() 888 if matchOutcomes, found := auth.matchOutcomes[user]; found { 889 matchOutcomes.add(&matchOutcome{ 890 time: refTime.UnixMilli(), 891 mid: mmid.MatchID, 892 outcome: violation, 893 value: value, 894 base: mmid.Base, 895 quote: mmid.Quote, 896 }) 897 score = auth.userScore(user) 898 auth.violationMtx.Unlock() 899 return 900 } 901 auth.violationMtx.Unlock() 902 903 // The user is currently not connected and authenticated. When the user logs 904 // back in, their history will be reloaded (loadUserScore) and their tier 905 // recomputed, but compute their score now from DB for the caller. 906 score, err := auth.loadUserScore(user) 907 if err != nil { 908 log.Errorf("Failed to load order and match outcomes for user %v: %v", user, err) 909 return 0 910 } 911 912 return 913 } 914 915 // SwapSuccess registers the successful completion of a swap by the given user. 916 // TODO: provide lots instead of value, or convert to lots somehow. But, Swapper 917 // has no clue about lot size, and neither does DB! 918 func (auth *AuthManager) SwapSuccess(user account.AccountID, mmid db.MarketMatchID, value uint64, redeemTime time.Time) { 919 score := auth.registerMatchOutcome(user, SwapSuccess, mmid, value, redeemTime) 920 rep, tierChanged, scoreChanged := auth.computeUserReputation(user, score) // may raise tier 921 effectiveTier := rep.EffectiveTier() 922 log.Debugf("Match success for user %v: strikes %d, bond tier %v => tier %v", 923 user, score, rep.BondedTier, effectiveTier) 924 if tierChanged { 925 log.Infof("SwapSuccess: tier change for user %v, strikes %d, bond tier %v => trading tier %v", 926 user, score, rep.BondedTier, effectiveTier) 927 go auth.sendTierChanged(user, rep, "successful swap completion") 928 } else if scoreChanged { 929 go auth.sendScoreChanged(user, rep) 930 } 931 } 932 933 // Inaction registers an inaction violation by the user at the given step. The 934 // refTime is time to which the at-fault user's inaction deadline for the match 935 // is referenced. e.g. For a swap that failed in TakerSwapCast, refTime would be 936 // the maker's redeem time, which is recorded in the DB when the server 937 // validates the maker's redemption and informs the taker, and is roughly when 938 // the actor was first able to take the missed action. 939 // TODO: provide lots instead of value, or convert to lots somehow. But, Swapper 940 // has no clue about lot size, and neither does DB! 941 func (auth *AuthManager) Inaction(user account.AccountID, misstep NoActionStep, mmid db.MarketMatchID, matchValue uint64, refTime time.Time, oid order.OrderID) { 942 violation := misstep.Violation() 943 if violation == ViolationInvalid { 944 log.Errorf("Invalid inaction step %d", misstep) 945 return 946 } 947 score := auth.registerMatchOutcome(user, misstep, mmid, matchValue, refTime) 948 949 // Recompute tier. 950 rep, tierChanged, scoreChanged := auth.computeUserReputation(user, score) 951 effectiveTier := rep.EffectiveTier() 952 log.Infof("Match failure for user %v: %q (badness %v), strikes %d, bond tier %v => trading tier %v", 953 user, violation, violation.Score(), score, rep.BondedTier, effectiveTier) 954 // If their tier sinks below 1, unbook their orders and send a note. 955 if tierChanged && effectiveTier < 1 { 956 details := fmt.Sprintf("swap %v failure (%v) for order %v, new tier = %d", 957 mmid.MatchID, misstep, oid, effectiveTier) 958 auth.Penalize(user, account.FailureToAct, details) 959 } 960 if tierChanged { 961 reason := fmt.Sprintf("swap failure for match %v order %v: %v", mmid.MatchID, oid, misstep) 962 go auth.sendTierChanged(user, rep, reason) 963 } else if scoreChanged { 964 go auth.sendScoreChanged(user, rep) 965 } 966 } 967 968 func (auth *AuthManager) registerPreimageOutcome(user account.AccountID, miss bool, oid order.OrderID, refTime time.Time) (score int32) { 969 auth.violationMtx.Lock() 970 piOutcomes, found := auth.preimgOutcomes[user] 971 if found { 972 piOutcomes.add(&preimageOutcome{ 973 time: refTime.UnixMilli(), 974 oid: oid, 975 miss: miss, 976 }) 977 score = auth.userScore(user) 978 auth.violationMtx.Unlock() 979 return 980 } 981 auth.violationMtx.Unlock() 982 983 // The user is currently not connected and authenticated. When the user logs 984 // back in, their history will be reloaded (loadUserScore) and their tier 985 // recomputed, but compute their score now from DB for the caller. 986 var err error 987 score, err = auth.loadUserScore(user) 988 if err != nil { 989 log.Errorf("Failed to load order and match outcomes for user %v: %v", user, err) 990 return 0 991 } 992 993 return 994 } 995 996 // PreimageSuccess registers an accepted preimage for the user. 997 func (auth *AuthManager) PreimageSuccess(user account.AccountID, epochEnd time.Time, oid order.OrderID) { 998 score := auth.registerPreimageOutcome(user, false, oid, epochEnd) 999 auth.computeUserReputation(user, score) // may raise tier, but no action needed 1000 } 1001 1002 // MissedPreimage registers a missed preimage violation by the user. 1003 func (auth *AuthManager) MissedPreimage(user account.AccountID, epochEnd time.Time, oid order.OrderID) { 1004 score := auth.registerPreimageOutcome(user, true, oid, epochEnd) 1005 if score < auth.penaltyThreshold { 1006 return 1007 } 1008 1009 // Recompute tier. 1010 rep, tierChanged, scoreChanged := auth.computeUserReputation(user, score) 1011 effectiveTier := rep.EffectiveTier() 1012 log.Debugf("MissedPreimage: user %v strikes %d, bond tier %v => trading tier %v", user, score, rep.BondedTier, effectiveTier) 1013 // If their tier sinks below 1, unbook their orders and send a note. 1014 if tierChanged && effectiveTier < 1 { 1015 details := fmt.Sprintf("preimage for order %v not provided upon request: new tier = %d", oid, effectiveTier) 1016 auth.Penalize(user, account.PreimageReveal, details) 1017 } 1018 if tierChanged { 1019 reason := fmt.Sprintf("preimage not provided upon request for order %v", oid) 1020 go auth.sendTierChanged(user, rep, reason) 1021 } else if scoreChanged { 1022 go auth.sendScoreChanged(user, rep) 1023 } 1024 } 1025 1026 // Penalize unbooks all of their orders, and notifies them of this action while 1027 // citing the provided rule that corresponds to their most recent infraction. 1028 // This method is to be used when a user's tier drops below 1. 1029 // NOTE: There is now a 'tierchange' route for *any* tier change, but this 1030 // method still handles unbooking of the user's orders. 1031 func (auth *AuthManager) Penalize(user account.AccountID, lastRule account.Rule, extraDetails string) { 1032 // Unbook all of the user's orders across all markets. 1033 auth.unbookUserOrders(user) 1034 1035 log.Debugf("User %v account penalized. Last rule broken = %v. Detail: %s", user, lastRule, extraDetails) 1036 1037 // Notify user of penalty. 1038 details := "Ordering has been suspended for this account. Post additional bond to offset violations." 1039 details = fmt.Sprintf("%s\nLast Broken Rule Details: %s\n%s", details, lastRule.Description(), extraDetails) 1040 penalty := &msgjson.Penalty{ 1041 Rule: lastRule, 1042 Time: uint64(time.Now().UnixMilli()), 1043 Details: details, 1044 } 1045 penaltyNote := &msgjson.PenaltyNote{ 1046 Penalty: penalty, 1047 } 1048 penaltyNote.Sig = auth.SignMsg(penaltyNote.Serialize()) 1049 note, err := msgjson.NewNotification(msgjson.PenaltyRoute, penaltyNote) 1050 if err != nil { 1051 log.Errorf("error creating penalty notification: %w", err) 1052 return 1053 } 1054 auth.Notify(user, note) 1055 } 1056 1057 // AcctStatus indicates if the user is presently connected and their tier. 1058 func (auth *AuthManager) AcctStatus(user account.AccountID) (connected bool, tier int64) { 1059 client := auth.user(user) 1060 if client == nil { 1061 // Load user info from DB. 1062 rep := auth.ComputeUserReputation(user) 1063 if rep != nil { 1064 tier = rep.EffectiveTier() 1065 } 1066 return 1067 } 1068 connected = true 1069 1070 client.mtx.Lock() 1071 tier = client.tier 1072 client.mtx.Unlock() 1073 1074 return 1075 } 1076 1077 // ForgiveMatchFail forgives a user for a specific match failure, potentially 1078 // allowing them to resume trading if their score becomes passing. NOTE: This 1079 // may become deprecated with mesh, unless matches may be forgiven in some 1080 // automatic network reconciliation process. 1081 func (auth *AuthManager) ForgiveMatchFail(user account.AccountID, mid order.MatchID) (forgiven, unbanned bool, err error) { 1082 // Forgive the specific match failure in the DB. 1083 forgiven, err = auth.storage.ForgiveMatchFail(mid) 1084 if err != nil { 1085 return 1086 } 1087 1088 // Reload outcomes from DB. NOTE: This does not use loadUserScore because we 1089 // also need to update the matchOutcomes map if the user is online. 1090 latestMatches, latestPreimageResults, latestFinished, err := auth.loadUserOutcomes(user) 1091 auth.violationMtx.Lock() 1092 _, online := auth.matchOutcomes[user] 1093 if online { 1094 auth.matchOutcomes[user] = latestMatches // other outcomes unchanged 1095 } 1096 auth.violationMtx.Unlock() 1097 1098 // Recompute the user's score. 1099 score, _, _ := auth.integrateOutcomes(latestMatches, latestPreimageResults, latestFinished) 1100 1101 // Recompute tier. 1102 rep, tierChanged, scoreChanged := auth.computeUserReputation(user, score) 1103 if tierChanged { 1104 go auth.sendTierChanged(user, rep, "swap failure forgiven") 1105 } else if scoreChanged { 1106 go auth.sendScoreChanged(user, rep) 1107 } 1108 1109 unbanned = rep.EffectiveTier() > 0 1110 1111 return 1112 } 1113 1114 // CreatePrepaidBonds generates pre-paid bonds. 1115 func (auth *AuthManager) CreatePrepaidBonds(n int, strength uint32, durSecs int64) ([][]byte, error) { 1116 coinIDs := make([][]byte, n) 1117 const prepaidBondIDLength = 16 1118 for i := 0; i < n; i++ { 1119 coinIDs[i] = encode.RandomBytes(prepaidBondIDLength) 1120 } 1121 lockTime := time.Now().Add(auth.bondExpiry).Add(time.Duration(durSecs) * time.Second) 1122 if err := auth.storage.StorePrepaidBonds(coinIDs, strength, lockTime.Unix()); err != nil { 1123 return nil, err 1124 } 1125 return coinIDs, nil 1126 } 1127 1128 // TODO: a way to manipulate/forgive cancellation rate violation. 1129 1130 // user gets the clientInfo for the specified account ID. 1131 func (auth *AuthManager) user(user account.AccountID) *clientInfo { 1132 auth.connMtx.RLock() 1133 defer auth.connMtx.RUnlock() 1134 return auth.users[user] 1135 } 1136 1137 // conn gets the clientInfo for the specified connection ID. 1138 func (auth *AuthManager) conn(conn comms.Link) *clientInfo { 1139 auth.connMtx.RLock() 1140 defer auth.connMtx.RUnlock() 1141 return auth.conns[conn.ID()] 1142 } 1143 1144 // sendTierChanged sends a tierchanged notification to an account. 1145 func (auth *AuthManager) sendTierChanged(acctID account.AccountID, rep *account.Reputation, reason string) { 1146 effectiveTier := rep.EffectiveTier() 1147 log.Debugf("Sending tierchanged notification to %v, new tier = %d, reason = %v", 1148 acctID, effectiveTier, reason) 1149 tierChangedNtfn := &msgjson.TierChangedNotification{ 1150 Tier: effectiveTier, 1151 Reputation: rep, 1152 Reason: reason, 1153 } 1154 auth.Sign(tierChangedNtfn) 1155 resp, err := msgjson.NewNotification(msgjson.TierChangeRoute, tierChangedNtfn) 1156 if err != nil { 1157 log.Error("TierChangeRoute encoding error: %v", err) 1158 return 1159 } 1160 if err = auth.Send(acctID, resp); err != nil { 1161 log.Warnf("Error sending tier changed notification to account %v: %v", acctID, err) 1162 // The user will need to 'connect' to see their current tier and bonds. 1163 } 1164 } 1165 1166 // sendScoreChanged sends a scorechanged notification to an account. 1167 func (auth *AuthManager) sendScoreChanged(acctID account.AccountID, rep *account.Reputation) { 1168 note := &msgjson.ScoreChangedNotification{ 1169 Reputation: *rep, 1170 } 1171 auth.Sign(note) 1172 resp, err := msgjson.NewNotification(msgjson.ScoreChangeRoute, note) 1173 if err != nil { 1174 log.Error("TierChangeRoute encoding error: %v", err) 1175 return 1176 } 1177 if err = auth.Send(acctID, resp); err != nil { 1178 log.Warnf("Error sending score changed notification to account %v: %v", acctID, err) 1179 // The user will need to 'connect' to see their current tier and bonds. 1180 } 1181 } 1182 1183 // sendBondExpired sends a bondexpired notification to an account. 1184 func (auth *AuthManager) sendBondExpired(acctID account.AccountID, bond *db.Bond, rep *account.Reputation) { 1185 effectiveTier := rep.EffectiveTier() 1186 log.Debugf("Sending bondexpired notification to %v for bond %v (%s), new tier = %d", 1187 acctID, coinIDString(bond.AssetID, bond.CoinID), dex.BipIDSymbol(bond.AssetID), effectiveTier) 1188 bondExpNtfn := &msgjson.BondExpiredNotification{ 1189 AssetID: bond.AssetID, 1190 BondCoinID: bond.CoinID, 1191 AccountID: acctID[:], 1192 Tier: effectiveTier, 1193 Reputation: rep, 1194 } 1195 auth.Sign(bondExpNtfn) 1196 resp, err := msgjson.NewNotification(msgjson.BondExpiredRoute, bondExpNtfn) 1197 if err != nil { 1198 log.Error("BondExpiredRoute encoding error: %v", err) 1199 return 1200 } 1201 if err = auth.Send(acctID, resp); err != nil { 1202 log.Warnf("Error sending bond expired notification to account %v: %v", acctID, err) 1203 // The user will need to 'connect' to see their current tier and bonds. 1204 } 1205 } 1206 1207 // checkBonds checks all connected users' bonds expiry and recomputes user tier 1208 // on change. This should be run on a ticker. 1209 func (auth *AuthManager) checkBonds() { 1210 lockTimeThresh := time.Now().Add(auth.bondExpiry).Unix() 1211 1212 checkClientBonds := func(client *clientInfo) ([]*db.Bond, *account.Reputation) { 1213 client.mtx.Lock() 1214 defer client.mtx.Unlock() 1215 pruned, bondTier := client.pruneBonds(lockTimeThresh) 1216 if len(pruned) == 0 { 1217 return nil, nil // no tier change 1218 } 1219 1220 auth.violationMtx.Lock() 1221 score := auth.userScore(client.acct.ID) 1222 auth.violationMtx.Unlock() 1223 1224 client.tier = auth.tier(bondTier, score) 1225 client.score = score 1226 1227 return pruned, auth.userReputation(bondTier, score) 1228 } 1229 1230 auth.connMtx.RLock() 1231 defer auth.connMtx.RUnlock() 1232 1233 type checkRes struct { 1234 rep *account.Reputation 1235 bonds []*db.Bond 1236 } 1237 expiredBonds := make(map[account.AccountID]checkRes) 1238 for acct, client := range auth.users { 1239 pruned, rep := checkClientBonds(client) 1240 if len(pruned) > 0 { 1241 log.Infof("Pruned %d expired bonds for user %v, new bond tier = %d, new trading tier = %d", 1242 len(pruned), acct, rep.BondedTier, client.tier) 1243 expiredBonds[acct] = checkRes{rep, pruned} 1244 } 1245 } 1246 1247 if len(expiredBonds) == 0 { 1248 return // skip the goroutine alloc 1249 } 1250 1251 auth.wg.Add(1) 1252 go func() { // godspeed 1253 defer auth.wg.Done() 1254 for acct, prunes := range expiredBonds { 1255 for _, bond := range prunes.bonds { 1256 if err := auth.storage.DeleteBond(bond.AssetID, bond.CoinID); err != nil { 1257 log.Errorf("Failed to delete expired bond %v (%s) for user %v: %v", 1258 coinIDString(bond.AssetID, bond.CoinID), dex.BipIDSymbol(bond.AssetID), acct, err) 1259 } 1260 auth.sendBondExpired(acct, bond, prunes.rep) 1261 } 1262 } 1263 }() 1264 } 1265 1266 // addBond registers a new active bond for an authenticated user. This only 1267 // updates their clientInfo.{bonds,tier} fields. It does not touch the DB. If 1268 // the user is not authenticated, it returns -1, -1. 1269 func (auth *AuthManager) addBond(user account.AccountID, bond *db.Bond) *account.Reputation { 1270 client := auth.user(user) 1271 if client == nil { 1272 return nil // offline 1273 } 1274 1275 auth.violationMtx.Lock() 1276 score := auth.userScore(user) 1277 auth.violationMtx.Unlock() 1278 1279 client.mtx.Lock() 1280 defer client.mtx.Unlock() 1281 1282 bondTier := client.addBond(bond) 1283 rep := auth.userReputation(bondTier, score) 1284 client.tier = rep.EffectiveTier() 1285 client.score = score 1286 1287 return rep 1288 } 1289 1290 // addClient adds the client to the users and conns maps, and stops any unbook 1291 // timers started when they last disconnected. 1292 func (auth *AuthManager) addClient(client *clientInfo) { 1293 auth.connMtx.Lock() 1294 defer auth.connMtx.Unlock() 1295 user := client.acct.ID 1296 if unbookTimer, found := auth.unbookers[user]; found { 1297 if unbookTimer.Stop() { 1298 log.Debugf("Stopped unbook timer for user %v", user) 1299 } 1300 delete(auth.unbookers, user) 1301 } 1302 1303 oldClient := auth.users[user] 1304 auth.users[user] = client 1305 1306 connID := client.conn.ID() 1307 auth.conns[connID] = client 1308 1309 // Now that the new conn ID is registered, disconnect any existing old link 1310 // unless it is the same. 1311 if oldClient != nil { 1312 oldConnID := oldClient.conn.ID() 1313 if oldConnID == connID { 1314 return // reused conn, just update maps 1315 } 1316 log.Warnf("User %v reauthorized from %v (id %d) with an existing connection from %v (id %d). Disconnecting the old one.", 1317 user, client.conn.Addr(), connID, oldClient.conn.Addr(), oldConnID) 1318 // When replacing with a new conn, manually deregister the old conn so 1319 // that when it disconnects it does not remove the new clientInfo. 1320 delete(auth.conns, oldConnID) 1321 oldClient.conn.Disconnect() 1322 } 1323 1324 // When the conn goes down, automatically unregister the client. 1325 go func() { 1326 <-client.conn.Done() 1327 log.Debugf("Link down: id=%d, ip=%s.", client.conn.ID(), client.conn.Addr()) 1328 auth.removeClient(client) // must stop if connID already removed 1329 }() 1330 } 1331 1332 // removeClient removes the client from the users and conns map, and sets a 1333 // timer to unbook all of the user's orders if they do not return within a 1334 // certain time. This is idempotent for a given conn ID. 1335 func (auth *AuthManager) removeClient(client *clientInfo) { 1336 auth.connMtx.Lock() 1337 defer auth.connMtx.Unlock() 1338 connID := client.conn.ID() 1339 _, connFound := auth.conns[connID] 1340 if !connFound { 1341 // conn already removed manually when this user made a new connection. 1342 // This user is still in the users map, so return. 1343 return 1344 } 1345 user := client.acct.ID 1346 delete(auth.users, user) 1347 delete(auth.conns, connID) 1348 client.conn.Disconnect() // in case not triggered by disconnect 1349 auth.unbookers[user] = time.AfterFunc(auth.miaUserTimeout, func() { auth.unbookUserOrders(user) }) 1350 1351 auth.violationMtx.Lock() 1352 delete(auth.matchOutcomes, user) 1353 delete(auth.preimgOutcomes, user) 1354 delete(auth.orderOutcomes, user) 1355 auth.violationMtx.Unlock() 1356 } 1357 1358 func matchStatusToViol(status order.MatchStatus) Violation { 1359 switch status { 1360 case order.NewlyMatched: 1361 return ViolationNoSwapAsMaker 1362 case order.MakerSwapCast: 1363 return ViolationNoSwapAsTaker 1364 case order.TakerSwapCast: 1365 return ViolationNoRedeemAsMaker 1366 case order.MakerRedeemed: 1367 return ViolationNoRedeemAsTaker 1368 case order.MatchComplete: 1369 return ViolationSwapSuccess // should be caught by Fail==false 1370 default: 1371 return ViolationInvalid 1372 } 1373 } 1374 1375 // loadUserOutcomes returns user's latest match and preimage outcomes from order 1376 // and swap data retrieved from the DB. 1377 func (auth *AuthManager) loadUserOutcomes(user account.AccountID) (*latestMatchOutcomes, *latestPreimageOutcomes, *latestOrders, error) { 1378 // Load the N most recent matches resulting in success or an at-fault match 1379 // revocation for the user. 1380 matchOutcomes, err := auth.storage.CompletedAndAtFaultMatchStats(user, ScoringMatchLimit) 1381 if err != nil { 1382 return nil, nil, nil, fmt.Errorf("CompletedAndAtFaultMatchStats: %w", err) 1383 } 1384 1385 // Load the count of preimage misses in the N most recently placed orders. 1386 piOutcomes, err := auth.storage.PreimageStats(user, scoringOrderLimit) 1387 if err != nil { 1388 return nil, nil, nil, fmt.Errorf("PreimageStats: %w", err) 1389 } 1390 1391 latestMatches := newLatestMatchOutcomes(ScoringMatchLimit) 1392 for _, mo := range matchOutcomes { 1393 // The Fail flag qualifies MakerRedeemed, which is always success for 1394 // maker, but fail for taker if revoked. 1395 v := ViolationSwapSuccess 1396 if mo.Fail { 1397 v = matchStatusToViol(mo.Status) 1398 } 1399 latestMatches.add(&matchOutcome{ 1400 time: mo.Time, 1401 mid: mo.ID, 1402 outcome: v, 1403 value: mo.Value, // Note: DB knows value, not number of lots! 1404 base: mo.Base, 1405 quote: mo.Quote, 1406 }) 1407 } 1408 1409 latestPreimageResults := newLatestPreimageOutcomes(scoringOrderLimit) 1410 for _, po := range piOutcomes { 1411 latestPreimageResults.add(&preimageOutcome{ 1412 time: po.Time, 1413 oid: po.ID, 1414 miss: po.Miss, 1415 }) 1416 } 1417 1418 // Retrieve the user's N latest finished (completed or canceled orders) 1419 // and store them in a latestOrders. 1420 orderOutcomes, err := auth.loadRecentFinishedOrders(user, cancelThreshWindow) 1421 if err != nil { 1422 log.Errorf("Unable to retrieve user's executed cancels and completed orders: %v", err) 1423 return nil, nil, nil, err 1424 } 1425 1426 return latestMatches, latestPreimageResults, orderOutcomes, nil 1427 } 1428 1429 // MatchOutcome is a JSON-friendly version of db.MatchOutcome. 1430 type MatchOutcome struct { 1431 ID dex.Bytes `json:"matchID"` 1432 Status string `json:"status"` 1433 Fail bool `json:"failed"` 1434 Stamp int64 `json:"stamp"` 1435 Value uint64 `json:"value"` 1436 BaseID uint32 `json:"baseID"` 1437 Quote uint32 `json:"quoteID"` 1438 } 1439 1440 // MatchFail is a failed match and the effect on the user's score. 1441 type MatchFail struct { 1442 ID dex.Bytes `json:"matchID"` 1443 Penalty uint32 `json:"penalty"` 1444 } 1445 1446 // AccountMatchOutcomesN generates a list of recent match outcomes for a user. 1447 func (auth *AuthManager) AccountMatchOutcomesN(user account.AccountID, n int) ([]*MatchOutcome, error) { 1448 dbOutcomes, err := auth.storage.CompletedAndAtFaultMatchStats(user, n) 1449 if err != nil { 1450 return nil, err 1451 } 1452 outcomes := make([]*MatchOutcome, len(dbOutcomes)) 1453 for i, o := range dbOutcomes { 1454 outcomes[i] = &MatchOutcome{ 1455 ID: o.ID[:], 1456 Status: o.Status.String(), 1457 Fail: o.Fail, 1458 Stamp: o.Time, 1459 Value: o.Value, 1460 BaseID: o.Base, 1461 Quote: o.Quote, 1462 } 1463 } 1464 return outcomes, nil 1465 } 1466 1467 func (auth *AuthManager) UserMatchFails(user account.AccountID, n int) ([]*MatchFail, error) { 1468 matchFails, err := auth.storage.UserMatchFails(user, n) 1469 if err != nil { 1470 return nil, err 1471 } 1472 fails := make([]*MatchFail, len(matchFails)) 1473 for i, fail := range matchFails { 1474 fails[i] = &MatchFail{ 1475 ID: fail.ID[:], 1476 Penalty: uint32(-1 * matchStatusToViol(fail.Status).Score()), 1477 } 1478 } 1479 return fails, nil 1480 } 1481 1482 // loadUserScore computes the user's current score from order and swap data 1483 // retrieved from the DB. Use this instead of userScore if the user is offline. 1484 func (auth *AuthManager) loadUserScore(user account.AccountID) (int32, error) { 1485 latestMatches, latestPreimageResults, latestFinished, err := auth.loadUserOutcomes(user) 1486 if err != nil { 1487 return 0, err 1488 } 1489 1490 score, _, _ := auth.integrateOutcomes(latestMatches, latestPreimageResults, latestFinished) 1491 return score, nil 1492 } 1493 1494 // handleConnect is the handler for the 'connect' route. The user is authorized, 1495 // a response is issued, and a clientInfo is created or updated. 1496 func (auth *AuthManager) handleConnect(conn comms.Link, msg *msgjson.Message) *msgjson.Error { 1497 connect := new(msgjson.Connect) 1498 err := msg.Unmarshal(&connect) 1499 if err != nil || connect == nil { 1500 return &msgjson.Error{ 1501 Code: msgjson.RPCParseError, 1502 Message: "error parsing connect request", 1503 } 1504 } 1505 if len(connect.AccountID) != account.HashSize { 1506 return &msgjson.Error{ 1507 Code: msgjson.AuthenticationError, 1508 Message: "authentication error. invalid account ID", 1509 } 1510 } 1511 var user account.AccountID 1512 copy(user[:], connect.AccountID[:]) 1513 lockTimeThresh := time.Now().Add(auth.bondExpiry).Truncate(time.Second) 1514 acctInfo, bonds := auth.storage.Account(user, lockTimeThresh) 1515 if acctInfo == nil { 1516 return &msgjson.Error{ 1517 Code: msgjson.AccountNotFoundError, 1518 Message: "no account found for account ID: " + connect.AccountID.String(), 1519 } 1520 } 1521 1522 // Tier 0 accounts may connect to complete swaps, etc. but not place new 1523 // orders. 1524 1525 // Authorize the account. 1526 sigMsg := connect.Serialize() 1527 err = checkSigS256(sigMsg, connect.SigBytes(), acctInfo.PubKey) 1528 if err != nil { 1529 return &msgjson.Error{ 1530 Code: msgjson.SignatureError, 1531 Message: "signature error: " + err.Error(), 1532 } 1533 } 1534 1535 // Check to see if there is already an existing client for this account. 1536 respHandlers := make(map[uint64]*respHandler) 1537 oldClient := auth.user(acctInfo.ID) 1538 if oldClient != nil { 1539 oldClient.mtx.Lock() 1540 respHandlers = oldClient.respHandlers 1541 oldClient.mtx.Unlock() 1542 } 1543 1544 // Compute the user's score, loading the preimage/order/match outcomes. 1545 latestMatches, latestPreimageResults, latestFinished, err := auth.loadUserOutcomes(user) 1546 if err != nil { 1547 log.Errorf("Failed to compute user %v score: %v", user, err) 1548 return &msgjson.Error{ 1549 Code: msgjson.RPCInternalError, 1550 Message: "DB error", 1551 } 1552 } 1553 score, successCount, piMissCount := auth.integrateOutcomes(latestMatches, latestPreimageResults, latestFinished) 1554 1555 successScore := successCount * successScore 1556 piMissScore := piMissCount * preimageMissScore 1557 // score = violationScore + piMissScore + successScore 1558 violationScore := score - piMissScore - successScore // work backwards as per above comment 1559 log.Debugf("User %v score = %d:%d (%d successes) - %d (violations) - %d (%d preimage misses) ", 1560 user, score, successScore, successCount, -violationScore, -piMissScore, piMissCount) 1561 1562 // Make outcome entries for the user. 1563 auth.violationMtx.Lock() 1564 auth.matchOutcomes[user] = latestMatches 1565 auth.preimgOutcomes[user] = latestPreimageResults 1566 auth.orderOutcomes[user] = latestFinished 1567 auth.violationMtx.Unlock() 1568 1569 client := &clientInfo{ 1570 acct: acctInfo, 1571 conn: conn, 1572 respHandlers: respHandlers, 1573 } 1574 1575 // Get the list of active orders for this user. 1576 activeOrderStatuses, err := auth.storage.ActiveUserOrderStatuses(user) 1577 if err != nil { 1578 log.Errorf("ActiveUserOrderStatuses(%v): %v", user, err) 1579 return &msgjson.Error{ 1580 Code: msgjson.RPCInternalError, 1581 Message: "DB error", 1582 } 1583 } 1584 1585 msgOrderStatuses := make([]*msgjson.OrderStatus, 0, len(activeOrderStatuses)) 1586 for _, orderStatus := range activeOrderStatuses { 1587 msgOrderStatuses = append(msgOrderStatuses, &msgjson.OrderStatus{ 1588 ID: orderStatus.ID.Bytes(), 1589 Status: uint16(orderStatus.Status), 1590 }) 1591 } 1592 1593 // Get the list of active matches for this user. 1594 matches, err := auth.storage.AllActiveUserMatches(user) 1595 if err != nil { 1596 log.Errorf("AllActiveUserMatches(%v): %v", user, err) 1597 return &msgjson.Error{ 1598 Code: msgjson.RPCInternalError, 1599 Message: "DB error", 1600 } 1601 } 1602 1603 // There may be as many as 2*len(matches) match messages if the user matched 1604 // with themself, but this is likely to be very rare outside of tests. 1605 msgMatches := make([]*msgjson.Match, 0, len(matches)) 1606 1607 // msgMatchForSide checks if the user is on the given side of the match, 1608 // appending the match to the slice if so. The Address and Side fields of 1609 // msgjson.Match will differ depending on the side. 1610 msgMatchForSide := func(match *db.MatchData, side order.MatchSide) { 1611 var addr string 1612 var oid []byte 1613 switch { 1614 case side == order.Maker && user == match.MakerAcct: 1615 addr = match.TakerAddr // counterparty 1616 oid = match.Maker[:] 1617 // sell = !match.TakerSell 1618 case side == order.Taker && user == match.TakerAcct: 1619 addr = match.MakerAddr // counterparty 1620 oid = match.Taker[:] 1621 // sell = match.TakerSell 1622 default: 1623 return 1624 } 1625 1626 msgMatches = append(msgMatches, &msgjson.Match{ 1627 OrderID: oid, 1628 MatchID: match.ID[:], 1629 Quantity: match.Quantity, 1630 Rate: match.Rate, 1631 ServerTime: uint64(match.Epoch.End().UnixMilli()), 1632 Address: addr, 1633 FeeRateBase: match.BaseRate, // contract txn fee rate if user is selling 1634 FeeRateQuote: match.QuoteRate, // contract txn fee rate if user is buying 1635 Status: uint8(match.Status), 1636 Side: uint8(side), 1637 }) 1638 } 1639 1640 // For each db match entry, create at least one msgjson.Match. 1641 activeMatchIDs := make(map[order.MatchID]bool, len(matches)) 1642 for _, match := range matches { 1643 activeMatchIDs[match.ID] = true 1644 msgMatchForSide(match, order.Maker) 1645 msgMatchForSide(match, order.Taker) 1646 } 1647 1648 conn.Authorized() 1649 1650 // Prepare bond info for response. 1651 var bondTier int64 1652 activeBonds := make([]*db.Bond, 0, len(bonds)) // some may have just expired 1653 msgBonds := make([]*msgjson.Bond, 0, len(bonds)) 1654 for _, bond := range bonds { 1655 // Double check the DB backend's thresholding. 1656 lockTime := time.Unix(bond.LockTime, 0) 1657 if lockTime.Before(lockTimeThresh) { 1658 log.Warnf("Loaded expired bond from DB (%v), lockTime %v is before %v", 1659 coinIDString(bond.AssetID, bond.CoinID), lockTime, lockTimeThresh) 1660 continue // will be expired on next prune 1661 } 1662 bondTier += int64(bond.Strength) 1663 expireTime := lockTime.Add(-auth.bondExpiry) 1664 msgBonds = append(msgBonds, &msgjson.Bond{ 1665 Version: bond.Version, 1666 Amount: uint64(bond.Amount), 1667 Expiry: uint64(expireTime.Unix()), 1668 CoinID: bond.CoinID, 1669 AssetID: bond.AssetID, 1670 Strength: bond.Strength, // Added with v2 reputation 1671 }) 1672 activeBonds = append(activeBonds, bond) 1673 } 1674 1675 // Ensure tier and filtered bonds agree. 1676 rep := auth.userReputation(bondTier, score) 1677 client.tier = rep.EffectiveTier() 1678 client.score = score 1679 client.bonds = activeBonds 1680 1681 // Sign and send the connect response. 1682 sig := auth.SignMsg(sigMsg) 1683 resp := &msgjson.ConnectResult{ 1684 Sig: sig, 1685 ActiveOrderStatuses: msgOrderStatuses, 1686 ActiveMatches: msgMatches, 1687 Score: score, 1688 ActiveBonds: msgBonds, 1689 Reputation: rep, 1690 } 1691 respMsg, err := msgjson.NewResponse(msg.ID, resp, nil) 1692 if err != nil { 1693 log.Errorf("handleConnect prepare response error: %v", err) 1694 return &msgjson.Error{ 1695 Code: msgjson.RPCInternalError, 1696 Message: "internal error", 1697 } 1698 } 1699 1700 err = conn.Send(respMsg) 1701 if err != nil { 1702 log.Error("Failed to send connect response: " + err.Error()) 1703 return nil 1704 } 1705 1706 log.Infof("Authenticated account %v from %v with %d active orders, %d active matches, tier = %v, "+ 1707 "bond tier = %v, score = %v", 1708 user, conn.Addr(), len(msgOrderStatuses), len(msgMatches), client.tier, bondTier, score) 1709 auth.addClient(client) 1710 1711 return nil 1712 } 1713 1714 func (auth *AuthManager) loadRecentFinishedOrders(aid account.AccountID, N int) (*latestOrders, error) { 1715 // Load the N latest successfully completed orders for the user. 1716 oids, compTimes, err := auth.storage.CompletedUserOrders(aid, N) 1717 if err != nil { 1718 return nil, err 1719 } 1720 1721 // Load the N latest executed cancel orders for the user. 1722 cancels, err := auth.storage.ExecutedCancelsForUser(aid, N) 1723 if err != nil { 1724 return nil, err 1725 } 1726 1727 // Create the sorted list with capacity. 1728 latestFinished := newLatestOrders(cancelThreshWindow) 1729 // Insert the completed orders. 1730 for i := range oids { 1731 latestFinished.add(&oidStamped{ 1732 OrderID: oids[i], 1733 time: compTimes[i], 1734 //target: nil, 1735 }) 1736 } 1737 // Insert the executed cancels, popping off older orders that do not fit in 1738 // the list. 1739 for _, c := range cancels { 1740 tid := c.TargetID 1741 latestFinished.add(&oidStamped{ 1742 OrderID: c.ID, 1743 time: c.MatchTime, 1744 target: &tid, 1745 epochGap: c.EpochGap, 1746 }) 1747 } 1748 1749 return latestFinished, nil 1750 } 1751 1752 // handleResponse handles all responses for AuthManager registered routes, 1753 // essentially wrapping response handlers and translating connection ID to 1754 // account ID. 1755 func (auth *AuthManager) handleResponse(conn comms.Link, msg *msgjson.Message) { 1756 client := auth.conn(conn) 1757 if client == nil { 1758 log.Errorf("response from unknown connection") 1759 return 1760 } 1761 handler := client.respHandler(msg.ID) 1762 if handler == nil { 1763 log.Debugf("(*AuthManager).handleResponse: unknown msg ID %d", msg.ID) 1764 errMsg, err := msgjson.NewResponse(msg.ID, nil, 1765 msgjson.NewError(msgjson.UnknownResponseID, "unknown response ID")) 1766 if err != nil { 1767 log.Errorf("failure creating unknown ID response error message: %v", err) 1768 } else { 1769 err := conn.Send(errMsg) 1770 if err != nil { 1771 log.Tracef("error sending response failure message: %v", err) 1772 auth.removeClient(client) 1773 // client.conn.Disconnect() // async removal 1774 } 1775 } 1776 return 1777 } 1778 handler.f(conn, msg) 1779 } 1780 1781 // marketMatches is an index of match IDs associated with a particular market. 1782 type marketMatches struct { 1783 base uint32 1784 quote uint32 1785 matchIDs map[order.MatchID]bool 1786 } 1787 1788 // add adds a match ID to the marketMatches. 1789 func (mm *marketMatches) add(matchID order.MatchID) bool { 1790 _, found := mm.matchIDs[matchID] 1791 mm.matchIDs[matchID] = true 1792 return !found 1793 } 1794 1795 // idList generates a []order.MatchID from the currently indexed match IDs. 1796 func (mm *marketMatches) idList() []order.MatchID { 1797 ids := make([]order.MatchID, 0, len(mm.matchIDs)) 1798 for matchID := range mm.matchIDs { 1799 ids = append(ids, matchID) 1800 } 1801 return ids 1802 } 1803 1804 // getTxData gets the tx data for the coin ID. 1805 func (auth *AuthManager) getTxData(assetID uint32, coinID []byte) ([]byte, error) { 1806 txDataSrc, found := auth.txDataSources[assetID] 1807 if !found { 1808 return nil, fmt.Errorf("no tx data source for asset ID %d", assetID) 1809 } 1810 return txDataSrc(coinID) 1811 } 1812 1813 // handleMatchStatus handles requests to the 'match_status' route. 1814 func (auth *AuthManager) handleMatchStatus(conn comms.Link, msg *msgjson.Message) *msgjson.Error { 1815 client := auth.conn(conn) 1816 if client == nil { 1817 return msgjson.NewError(msgjson.UnauthorizedConnection, 1818 "cannot use route 'match_status' on an unauthorized connection") 1819 } 1820 var matchReqs []msgjson.MatchRequest 1821 err := msg.Unmarshal(&matchReqs) 1822 if err != nil || matchReqs == nil /* null Payload */ { 1823 return msgjson.NewError(msgjson.RPCParseError, "error parsing match_status request") 1824 } 1825 // NOTE: If len(matchReqs)==0 but not nil, Payload was `[]`, demanding a 1826 // positive response with `[]` in ResponsePayload.Result. 1827 1828 mkts := make(map[string]*marketMatches) 1829 var count int 1830 for _, req := range matchReqs { 1831 mkt, err := dex.MarketName(req.Base, req.Quote) 1832 if err != nil { 1833 return msgjson.NewError(msgjson.InvalidRequestError, "market with base=%d, quote=%d is not known", req.Base, req.Quote) 1834 } 1835 if len(req.MatchID) != order.MatchIDSize { 1836 return msgjson.NewError(msgjson.InvalidRequestError, "match ID is wrong length: %s", req.MatchID) 1837 } 1838 mktMatches, found := mkts[mkt] 1839 if !found { 1840 mktMatches = &marketMatches{ 1841 base: req.Base, 1842 quote: req.Quote, 1843 matchIDs: make(map[order.MatchID]bool), 1844 } 1845 mkts[mkt] = mktMatches 1846 } 1847 var matchID order.MatchID 1848 copy(matchID[:], req.MatchID) 1849 if mktMatches.add(matchID) { 1850 count++ 1851 } 1852 } 1853 1854 results := make([]*msgjson.MatchStatusResult, 0, count) // should be non-nil even for count==0 1855 for _, mm := range mkts { 1856 statuses, err := auth.storage.MatchStatuses(client.acct.ID, mm.base, mm.quote, mm.idList()) 1857 // no results is not an error 1858 if err != nil { 1859 log.Errorf("MatchStatuses error: acct = %s, base = %d, quote = %d, matchIDs = %v: %v", 1860 client.acct.ID, mm.base, mm.quote, mm.matchIDs, err) 1861 return msgjson.NewError(msgjson.RPCInternalError, "DB error") 1862 } 1863 for _, status := range statuses { 1864 var makerTxData, takerTxData []byte 1865 var assetID uint32 1866 switch { 1867 case status.IsTaker && status.Status == order.MakerSwapCast: 1868 assetID = mm.base 1869 if status.TakerSell { 1870 assetID = mm.quote 1871 } 1872 makerTxData, err = auth.getTxData(assetID, status.MakerSwap) 1873 if err != nil { 1874 log.Errorf("failed to get maker tx data for %s %s: %v", dex.BipIDSymbol(assetID), 1875 coinIDString(assetID, status.MakerSwap), err) 1876 return msgjson.NewError(msgjson.RPCInternalError, "blockchain retrieval error") 1877 } 1878 case status.IsMaker && status.Status == order.TakerSwapCast: 1879 assetID = mm.quote 1880 if status.TakerSell { 1881 assetID = mm.base 1882 } 1883 takerTxData, err = auth.getTxData(assetID, status.TakerSwap) 1884 if err != nil { 1885 log.Errorf("failed to get taker tx data for %s %s: %v", dex.BipIDSymbol(assetID), 1886 coinIDString(assetID, status.TakerSwap), err) 1887 return msgjson.NewError(msgjson.RPCInternalError, "blockchain retrieval error") 1888 } 1889 } 1890 1891 results = append(results, &msgjson.MatchStatusResult{ 1892 MatchID: status.ID.Bytes(), 1893 Status: uint8(status.Status), 1894 MakerContract: status.MakerContract, 1895 TakerContract: status.TakerContract, 1896 MakerSwap: status.MakerSwap, 1897 TakerSwap: status.TakerSwap, 1898 MakerRedeem: status.MakerRedeem, 1899 TakerRedeem: status.TakerRedeem, 1900 Secret: status.Secret, 1901 Active: status.Active, 1902 MakerTxData: makerTxData, 1903 TakerTxData: takerTxData, 1904 }) 1905 } 1906 } 1907 1908 log.Tracef("%d results for %d requested match statuses, acct = %s", 1909 len(results), len(matchReqs), client.acct.ID) 1910 1911 resp, err := msgjson.NewResponse(msg.ID, results, nil) 1912 if err != nil { 1913 log.Errorf("NewResponse error: %v", err) 1914 return msgjson.NewError(msgjson.RPCInternalError, "Internal error") 1915 } 1916 1917 err = conn.Send(resp) 1918 if err != nil { 1919 log.Error("error sending match_status response: " + err.Error()) 1920 } 1921 return nil 1922 } 1923 1924 // marketOrders is an index of order IDs associated with a particular market. 1925 type marketOrders struct { 1926 base uint32 1927 quote uint32 1928 orderIDs map[order.OrderID]bool 1929 } 1930 1931 // add adds a match ID to the marketOrders. 1932 func (mo *marketOrders) add(oid order.OrderID) bool { 1933 _, found := mo.orderIDs[oid] 1934 mo.orderIDs[oid] = true 1935 return !found 1936 } 1937 1938 // idList generates a []order.OrderID from the currently indexed order IDs. 1939 func (mo *marketOrders) idList() []order.OrderID { 1940 ids := make([]order.OrderID, 0, len(mo.orderIDs)) 1941 for oid := range mo.orderIDs { 1942 ids = append(ids, oid) 1943 } 1944 return ids 1945 } 1946 1947 // handleOrderStatus handles requests to the 'order_status' route. 1948 func (auth *AuthManager) handleOrderStatus(conn comms.Link, msg *msgjson.Message) *msgjson.Error { 1949 client := auth.conn(conn) 1950 if client == nil { 1951 return msgjson.NewError(msgjson.UnauthorizedConnection, 1952 "cannot use route 'order_status' on an unauthorized connection") 1953 } 1954 1955 var orderReqs []*msgjson.OrderStatusRequest 1956 err := msg.Unmarshal(&orderReqs) 1957 if err != nil { 1958 return msgjson.NewError(msgjson.RPCParseError, "error parsing order_status request") 1959 } 1960 if len(orderReqs) == 0 { // includes null and [] Payload 1961 return msgjson.NewError(msgjson.InvalidRequestError, "no order id provided") 1962 } 1963 if len(orderReqs) > maxIDsPerOrderStatusRequest { 1964 return msgjson.NewError(msgjson.InvalidRequestError, "cannot request statuses for more than %v orders", 1965 maxIDsPerOrderStatusRequest) 1966 } 1967 1968 mkts := make(map[string]*marketOrders) 1969 var uniqueReqsCount int 1970 for _, req := range orderReqs { 1971 mkt, err := dex.MarketName(req.Base, req.Quote) 1972 if err != nil { 1973 return msgjson.NewError(msgjson.InvalidRequestError, "market with base=%d, quote=%d is not known", req.Base, req.Quote) 1974 } 1975 if len(req.OrderID) != order.OrderIDSize { 1976 return msgjson.NewError(msgjson.InvalidRequestError, "order ID is wrong length: %s", req.OrderID) 1977 } 1978 mktOrders, found := mkts[mkt] 1979 if !found { 1980 mktOrders = &marketOrders{ 1981 base: req.Base, 1982 quote: req.Quote, 1983 orderIDs: make(map[order.OrderID]bool), 1984 } 1985 mkts[mkt] = mktOrders 1986 } 1987 var oid order.OrderID 1988 copy(oid[:], req.OrderID) 1989 if mktOrders.add(oid) { 1990 uniqueReqsCount++ 1991 } 1992 } 1993 1994 results := make([]*msgjson.OrderStatus, 0, uniqueReqsCount) 1995 for _, mm := range mkts { 1996 orderStatuses, err := auth.storage.UserOrderStatuses(client.acct.ID, mm.base, mm.quote, mm.idList()) 1997 // no results is not an error 1998 if err != nil { 1999 log.Errorf("OrderStatuses error: acct = %s, base = %d, quote = %d, orderIDs = %v: %v", 2000 client.acct.ID, mm.base, mm.quote, mm.orderIDs, err) 2001 return msgjson.NewError(msgjson.RPCInternalError, "DB error") 2002 } 2003 for _, orderStatus := range orderStatuses { 2004 results = append(results, &msgjson.OrderStatus{ 2005 ID: orderStatus.ID.Bytes(), 2006 Status: uint16(orderStatus.Status), 2007 }) 2008 } 2009 } 2010 2011 log.Tracef("%d results for %d requested order statuses, acct = %s", 2012 len(results), uniqueReqsCount, client.acct.ID) 2013 2014 resp, err := msgjson.NewResponse(msg.ID, results, nil) 2015 if err != nil { 2016 log.Errorf("NewResponse error: %v", err) 2017 return msgjson.NewError(msgjson.RPCInternalError, "Internal error") 2018 } 2019 2020 err = conn.Send(resp) 2021 if err != nil { 2022 log.Error("error sending order_status response: " + err.Error()) 2023 } 2024 return nil 2025 } 2026 2027 func coinIDString(assetID uint32, coinID []byte) string { 2028 s, err := asset.DecodeCoinID(assetID, coinID) 2029 if err != nil { 2030 return "unparsed:" + hex.EncodeToString(coinID) 2031 } 2032 return s 2033 }