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  }