decred.org/dcrdex@v1.0.5/client/core/status.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 core
     5  
     6  import (
     7  	"fmt"
     8  	"sync"
     9  
    10  	"decred.org/dcrdex/client/asset"
    11  	"decred.org/dcrdex/dex/msgjson"
    12  	"decred.org/dcrdex/dex/order"
    13  )
    14  
    15  // statusResolutionID is just a string with the basic information about a match.
    16  // This is used often while logging during status resolution.
    17  func statusResolutionID(dc *dexConnection, trade *trackedTrade, match *matchTracker) string {
    18  	return fmt.Sprintf("host = %s, order = %s, match = %s", dc.acct.host, trade.ID(), match)
    19  }
    20  
    21  // resolveMatchConflicts attempts to resolve conflicts between the server's
    22  // reported match status and our own. This involves a 'match_status' request to
    23  // the server and possibly some wallet operations. ResolveMatchConflicts will
    24  // block until resolution is complete, with the exception of the resolvers that
    25  // perform asynchronous contract auditing. Trades are processed concurrently,
    26  // with matches resolved sequentially for a given trade.
    27  func (c *Core) resolveMatchConflicts(dc *dexConnection, statusConflicts map[order.OrderID]*matchStatusConflict) {
    28  
    29  	statusRequests := make([]*msgjson.MatchRequest, 0, len(statusConflicts))
    30  	for _, conflict := range statusConflicts {
    31  		for _, match := range conflict.matches {
    32  			statusRequests = append(statusRequests, &msgjson.MatchRequest{
    33  				Base:    conflict.trade.Base(),
    34  				Quote:   conflict.trade.Quote(),
    35  				MatchID: match.MatchID[:],
    36  			})
    37  		}
    38  	}
    39  
    40  	var msgStatuses []*msgjson.MatchStatusResult
    41  	err := sendRequest(dc.WsConn, msgjson.MatchStatusRoute, statusRequests, &msgStatuses, DefaultResponseTimeout)
    42  	if err != nil {
    43  		c.log.Errorf("match_status request error for %s requesting %d match statuses: %v",
    44  			dc.acct.host, len(statusRequests), err)
    45  		return
    46  	}
    47  
    48  	// Index the matches by match ID.
    49  	resMap := make(map[order.MatchID]*msgjson.MatchStatusResult, len(msgStatuses))
    50  	for _, msgStatus := range msgStatuses {
    51  		var matchID order.MatchID
    52  		copy(matchID[:], msgStatus.MatchID)
    53  		resMap[matchID] = msgStatus
    54  	}
    55  
    56  	var wg sync.WaitGroup
    57  	for _, conflict := range statusConflicts {
    58  		wg.Add(1)
    59  		go func(trade *trackedTrade, matches []*matchTracker) {
    60  			defer wg.Done()
    61  			trade.mtx.Lock()
    62  			defer trade.mtx.Unlock()
    63  			for _, match := range matches {
    64  				if srvData := resMap[match.MatchID]; srvData != nil {
    65  					c.resolveConflictWithServerData(dc, trade, match, srvData)
    66  					continue
    67  				}
    68  				// Server reported the match as active in the connect response,
    69  				// but it was revoked in the short period since then.
    70  				c.log.Errorf("Server did not report a status for match during resolution. %s",
    71  					statusResolutionID(dc, trade, match))
    72  				// revokeMatch only returns an error for a missing match ID, and
    73  				// we already checked in compareServerMatches.
    74  				_ = trade.revokeMatch(match.MatchID, false)
    75  			}
    76  		}(conflict.trade, conflict.matches)
    77  	}
    78  
    79  	wg.Wait()
    80  }
    81  
    82  // The matchConflictResolver is unique to a MatchStatus pair and handles
    83  // attempts to resolve a conflict between our match state and the state reported
    84  // by the server. A matchConflictResolver may update the MetaMatch, but need
    85  // not save the changes to persistent storage. Changes will be saved by
    86  // resolveConflictWithServerData.
    87  type matchConflictResolver func(*dexConnection, *trackedTrade, *matchTracker, *msgjson.MatchStatusResult)
    88  
    89  // conflictResolvers are the resolvers specified for each MatchStatus combo.
    90  var conflictResolvers = []struct {
    91  	ours, servers order.MatchStatus
    92  	resolver      matchConflictResolver
    93  }{
    94  	// Our status         Server's status      Resolver
    95  	{order.NewlyMatched, order.MakerSwapCast, resolveMissedMakerAudit},
    96  	{order.MakerSwapCast, order.NewlyMatched, resolveServerMissedMakerInit},
    97  	{order.MakerSwapCast, order.TakerSwapCast, resolveMissedTakerAudit},
    98  	{order.TakerSwapCast, order.MakerSwapCast, resolveServerMissedTakerInit},
    99  	{order.TakerSwapCast, order.MakerRedeemed, resolveMissedMakerRedemption},
   100  	{order.MakerRedeemed, order.TakerSwapCast, resolveServerMissedMakerRedeem},
   101  	{order.MakerRedeemed, order.MatchComplete, resolveMatchComplete},
   102  	{order.MatchComplete, order.MakerRedeemed, resolveServerMissedTakerRedeem},
   103  	{order.MatchConfirmed, order.MakerRedeemed, resolveServerMissedTakerRedeem},
   104  }
   105  
   106  // conflictResolver is a getter for a matchConflictResolver for the specified
   107  // MatchStatus combination. If there is no resolver for this combination, a
   108  // nil resolver will be returned.
   109  func conflictResolver(ours, servers order.MatchStatus) matchConflictResolver {
   110  	for _, r := range conflictResolvers {
   111  		if r.ours == ours && r.servers == servers {
   112  			return r.resolver
   113  		}
   114  	}
   115  	return nil
   116  }
   117  
   118  // resolveConflictWithServerData compares the match status with the server's
   119  // match_status data. The trackedTrade.mtx is locked for the duration of
   120  // resolution. If the conflict cannot be resolved, the match will be
   121  // self-revoked.
   122  func (c *Core) resolveConflictWithServerData(dc *dexConnection, trade *trackedTrade, match *matchTracker, srvData *msgjson.MatchStatusResult) {
   123  	srvStatus := order.MatchStatus(srvData.Status)
   124  	if srvStatus < order.MatchComplete && !srvData.Active {
   125  		// Server has revoked the match. We'll still go through
   126  		// resolveConflictWithServerData to collect any extra data the
   127  		// server has, but setting ServerRevoked will prevent us from
   128  		// trying to update the state with the server.
   129  		match.MetaData.Proof.ServerRevoked = true
   130  	}
   131  
   132  	if srvStatus == match.Status || match.MetaData.Proof.IsRevoked() {
   133  		// On startup, there's no chance for a tick between the connect request
   134  		// and the match_status request, so this would be unlikely. But if not
   135  		// during startup, and a tick has snuck in and resolved our status
   136  		// conflict already (either by refunding or via resendPendingRequests),
   137  		// that's OK.
   138  		return
   139  	}
   140  
   141  	logID := statusResolutionID(dc, trade, match)
   142  
   143  	if resolver := conflictResolver(match.Status, srvStatus); resolver != nil {
   144  		resolver(dc, trade, match, srvData)
   145  	} else {
   146  		// We don't know how to handle this. Set the swapErr, and self-revoke
   147  		// the match. This condition would be virtually impossible, because it
   148  		// would mean that the client and server were at least two steps out of
   149  		// sync.
   150  		match.MetaData.Proof.SelfRevoked = true
   151  		match.swapErr = fmt.Errorf("status conflict (%s -> %s) has no handler. %s",
   152  			match.Status, srvStatus, logID)
   153  		c.log.Error(match.swapErr)
   154  		err := c.db.UpdateMatch(&match.MetaMatch)
   155  		if err != nil {
   156  			c.log.Errorf("error updating database after self revocation for no conflict handler for %s: %v", logID, err)
   157  		}
   158  	}
   159  
   160  	// Store and match data updates in the DB.
   161  	err := c.db.UpdateMatch(&match.MetaMatch)
   162  	if err != nil {
   163  		c.log.Errorf("error updating database after successful match resolution for %s: %v", logID, err)
   164  	}
   165  }
   166  
   167  // resolveMissedMakerAudit is a matchConflictResolver to handle the case when
   168  // our status is NewlyMatched, but the server is at MakerSwapCast. If we are the
   169  // taker, we likely missed an audit request and we can process the match_status
   170  // data to get caught up.
   171  func resolveMissedMakerAudit(dc *dexConnection, trade *trackedTrade, match *matchTracker, srvData *msgjson.MatchStatusResult) {
   172  	logID := statusResolutionID(dc, trade, match)
   173  	var err error
   174  	defer func() {
   175  		if err != nil {
   176  			match.MetaData.Proof.SelfRevoked = true
   177  			dc.log.Error(err)
   178  		}
   179  	}()
   180  
   181  	// We can handle this if we're the taker.
   182  	if match.Side == order.Maker {
   183  		err = fmt.Errorf("Server is reporting match in MakerSwapCast, but we're the maker and haven't sent a swap. %s", logID)
   184  		return
   185  	}
   186  	// We probably missed an audit request.
   187  	if len(srvData.MakerSwap) == 0 {
   188  		err = fmt.Errorf("Server is reporting a match with status MakerSwapCast, but didn't include a coin ID for the swap. %s", logID)
   189  		return
   190  	}
   191  	if len(srvData.MakerContract) == 0 {
   192  		err = fmt.Errorf("Server is reporting a match with status MakerSwapCast, but didn't include the contract data. %s", logID)
   193  		return
   194  	}
   195  
   196  	go func() {
   197  		err := trade.auditContract(match, srvData.MakerSwap, srvData.MakerContract, srvData.MakerTxData)
   198  		if err != nil {
   199  			dc.log.Errorf("auditContract error during match status resolution (revoking match). %s: %v", logID, err)
   200  			trade.mtx.Lock()
   201  			defer trade.mtx.Unlock()
   202  			match.MetaData.Proof.SelfRevoked = true
   203  			err = trade.db.UpdateMatch(&match.MetaMatch)
   204  			if err != nil {
   205  				trade.dc.log.Errorf("Error updating database for match %s: %v", match, err)
   206  			}
   207  		}
   208  	}()
   209  }
   210  
   211  // resolveMissedTakerAudit is a matchConflictResolver to handle the case when
   212  // our status is MakerSwapCast, but the server is at TakerSwapCast. If we are
   213  // the maker, we likely missed an audit request and we can process the
   214  // match_status data to get caught up.
   215  func resolveMissedTakerAudit(dc *dexConnection, trade *trackedTrade, match *matchTracker, srvData *msgjson.MatchStatusResult) {
   216  	logID := statusResolutionID(dc, trade, match)
   217  	var err error
   218  	defer func() {
   219  		if err != nil {
   220  			match.MetaData.Proof.SelfRevoked = true
   221  			dc.log.Error(err)
   222  		}
   223  	}()
   224  	// This is nonsensical if we're the taker.
   225  	if match.Side == order.Taker {
   226  		err = fmt.Errorf("Server is reporting match in TakerSwapCast, but we're the taker and haven't sent a swap. %s", logID)
   227  		return
   228  	}
   229  	// We probably missed an audit request.
   230  	if len(srvData.TakerSwap) == 0 {
   231  		err = fmt.Errorf("Server is reporting a match with status TakerSwapCast, but didn't include a coin ID for the swap. %s", logID)
   232  		return
   233  	}
   234  	if len(srvData.TakerContract) == 0 {
   235  		err = fmt.Errorf("Server is reporting a match with status TakerSwapCast, but didn't include the contract data. %s", logID)
   236  		return
   237  	}
   238  
   239  	go func() {
   240  		err := trade.auditContract(match, srvData.TakerSwap, srvData.TakerContract, srvData.TakerTxData)
   241  		if err != nil {
   242  			dc.log.Errorf("auditContract error during match status resolution (revoking match). %s: %v", logID, err)
   243  			trade.mtx.Lock()
   244  			defer trade.mtx.Unlock()
   245  			match.MetaData.Proof.SelfRevoked = true
   246  			err = trade.db.UpdateMatch(&match.MetaMatch)
   247  			if err != nil {
   248  				trade.dc.log.Errorf("Error updating database for match %s: %v", match, err)
   249  			}
   250  		}
   251  	}()
   252  }
   253  
   254  // resolveServerMissedMakerInit is a matchConflictResolver to handle the case
   255  // when our status is MakerSwapCast, but the server is at NewlyMatched. If we're
   256  // the maker, we probably encountered an issue while sending our init request,
   257  // so we'll defer to resendPendingRequests to handle it in the next tick.
   258  func resolveServerMissedMakerInit(dc *dexConnection, trade *trackedTrade, match *matchTracker, srvData *msgjson.MatchStatusResult) {
   259  	logID := statusResolutionID(dc, trade, match)
   260  	// If we're not the maker, there's nothing we can do.
   261  	if match.Side != order.Maker {
   262  		dc.log.Errorf("Server reporting no maker swap, but they've already sent us the swap info. self-revoking. %s", logID)
   263  		match.MetaData.Proof.SelfRevoked = true
   264  		return
   265  	}
   266  	// If we don't have a server acknowledgment, that case will be picked up in
   267  	// resendPendingRequests at the next tick.
   268  	if len(match.MetaData.Proof.Auth.InitSig) == 0 {
   269  		return
   270  	}
   271  	// On the other hand, if we do have an acknowledgement from the server,
   272  	// this appears to be a server error, and we should just revoke the match
   273  	// and wait to refund.
   274  	dc.log.Errorf("Server appears to have lost our (maker's) init data after acknowledgement. self-revoking order. %s", logID)
   275  	match.MetaData.Proof.SelfRevoked = true
   276  }
   277  
   278  // resolveServerMissedTakerInit is a matchConflictResolver to handle the case
   279  // when our status is TakerSwapCast, but the server is at MakerSwapCast. If
   280  // we're the taker, the server likely missed our init request, so we'll defer to
   281  // resendPendingRequests to handle it in the next tick.
   282  func resolveServerMissedTakerInit(dc *dexConnection, trade *trackedTrade, match *matchTracker, srvData *msgjson.MatchStatusResult) {
   283  	logID := statusResolutionID(dc, trade, match)
   284  	// If we're not the taker, there's nothing we can do.
   285  	if match.Side != order.Taker {
   286  		dc.log.Errorf("Server reporting no taker swap, but they've already sent us the swap info. self-revoking. %s", logID)
   287  		match.MetaData.Proof.SelfRevoked = true
   288  		return
   289  	}
   290  	// If we don't have a server acknowledgment, that case will be picked up in
   291  	// resendPendingRequests at the next tick.
   292  	if len(match.MetaData.Proof.Auth.InitSig) == 0 {
   293  		return
   294  	}
   295  	// On the other hand, if we do have an acknowledgement from the server,
   296  	// this appears to be a server error, and we should just revoke the match
   297  	// and wait to refund.
   298  	dc.log.Errorf("Server appears to have lost our (taker's) init data after acknowledgement. self-revoking order. %s", logID)
   299  	match.MetaData.Proof.SelfRevoked = true
   300  }
   301  
   302  // resolveMissedMakerRedemption is a matchConflictResolver to handle the case
   303  // when our status is TakerSwapCast, but the server is at MakerRedeemed. If
   304  // we're the taker, we probably missed the redemption request from the server,
   305  // and we can process the match_status data to get caught up.
   306  func resolveMissedMakerRedemption(dc *dexConnection, trade *trackedTrade, match *matchTracker, srvData *msgjson.MatchStatusResult) {
   307  	logID := statusResolutionID(dc, trade, match)
   308  	var err error
   309  	defer func() {
   310  		if err != nil {
   311  			match.MetaData.Proof.SelfRevoked = true
   312  			dc.log.Error(err)
   313  		}
   314  	}()
   315  	// If we're the maker, this state is nonsense. Just revoke the match for
   316  	// good measure.
   317  	if match.Side == order.Maker {
   318  		coinStr, _ := asset.DecodeCoinID(trade.wallets.toWallet.AssetID, srvData.MakerRedeem)
   319  		err = fmt.Errorf("server reported match status MakerRedeemed, but we're the maker and we don't have redemption data."+
   320  			" self-revoking. %s, reported coin = %s", logID, coinStr)
   321  		return
   322  	}
   323  	// If we're the taker, grab the redemption data and progress the status.
   324  	if len(srvData.MakerRedeem) == 0 {
   325  		err = fmt.Errorf("Server reporting status MakerRedeemed, but not reporting "+
   326  			"a redemption coin ID. self-revoking. %s", logID)
   327  		return
   328  	}
   329  	if len(srvData.Secret) == 0 {
   330  		err = fmt.Errorf("Server reporting status MakerRedeemed, but not reporting "+
   331  			"a secret. self-revoking. %s", logID)
   332  		return
   333  	}
   334  	if err = trade.processMakersRedemption(match, srvData.MakerRedeem, srvData.Secret); err != nil {
   335  		err = fmt.Errorf("error processing maker's redemption data during match status resolution. "+
   336  			"self-revoking. %s", logID)
   337  	}
   338  }
   339  
   340  // resolveMatchComplete is a matchConflictResolver to handle the case when our
   341  // status is MakerRedeemed, but the server is at MatchComplete. Since the server
   342  // does not send redemption requests to the maker following taker redeem, this
   343  // indicates the match status was just not updated after sending our redeem.
   344  func resolveMatchComplete(dc *dexConnection, trade *trackedTrade, match *matchTracker, srvData *msgjson.MatchStatusResult) {
   345  	logID := statusResolutionID(dc, trade, match)
   346  	// If we're the taker, this state is nonsense. Just revoke the match for
   347  	// good measure.
   348  	if match.Side == order.Taker {
   349  		match.MetaData.Proof.SelfRevoked = true
   350  		coinStr, _ := asset.DecodeCoinID(trade.wallets.toWallet.AssetID, srvData.TakerRedeem)
   351  		dc.log.Error("server reported match status MatchComplete, but we're the taker and we don't have redemption data."+
   352  			" self-revoking. %s, reported coin = %s", logID, coinStr)
   353  		return
   354  	}
   355  	// As maker, set it to MatchComplete. We no longer expect to receive taker
   356  	// redeem info.
   357  	dc.log.Warnf("Server reporting MatchComplete while we (maker) have it as MakerRedeemed. Resolved. Detail: %v", logID)
   358  	match.Status = order.MatchComplete
   359  }
   360  
   361  // resolveServerMissedMakerRedeem is a matchConflictResolver to handle the case
   362  // when our status is MakerRedeemed, but the server is at TakerSwapCast. If
   363  // we're the maker, the server probably missed our redeem request, so we'll
   364  // defer to resendPendingRequests to handle it in the next tick.
   365  func resolveServerMissedMakerRedeem(dc *dexConnection, trade *trackedTrade, match *matchTracker, srvData *msgjson.MatchStatusResult) {
   366  	logID := statusResolutionID(dc, trade, match)
   367  	// If we're not the maker, we can't do anything about this.
   368  	if match.Side != order.Maker {
   369  		dc.log.Errorf("server reporting no maker redeem, but they've already sent us the redemption info. self-revoking. %s", logID)
   370  		match.MetaData.Proof.SelfRevoked = true
   371  		return
   372  	}
   373  	// We are the maker, if we don't have an ack from the server, this will be
   374  	// picked up in resendPendingRequests during the next tick.
   375  	if len(match.MetaData.Proof.Auth.RedeemSig) == 0 {
   376  		return
   377  	}
   378  	// Otherwise, it appears that the server has acked, and then lost our redeem
   379  	// data. Just revoke.
   380  	dc.log.Errorf("server reporting no maker redeem, but we are the maker and we have a valid ack. self-revoking. %s", logID)
   381  	match.MetaData.Proof.SelfRevoked = true
   382  }
   383  
   384  // resolveServerMissedTakerRedeem is a matchConflictResolver to handle the case
   385  // when our status is MatchComplete, but the server is at MakerRedeemed. If
   386  // we're the taker, the server probably missed our redeem request, so we'll
   387  // defer to resendPendingRequests to handle it in the next tick.
   388  func resolveServerMissedTakerRedeem(dc *dexConnection, trade *trackedTrade, match *matchTracker, srvData *msgjson.MatchStatusResult) {
   389  	logID := statusResolutionID(dc, trade, match)
   390  	// If we're the Maker, we really are done. The server is in MakerRedeemed as
   391  	// it's waiting on the taker.
   392  	if match.Side == order.Maker {
   393  		return
   394  	}
   395  	// We are the taker. If we don't have an ack from the server, this will be
   396  	// picked up in resendPendingRequests during the next tick.
   397  	if len(match.MetaData.Proof.Auth.RedeemSig) == 0 {
   398  		return
   399  	}
   400  	// Otherwise, it appears that the server has acked, and then lost our redeem
   401  	// data. Just revoke.
   402  	dc.log.Errorf("server reporting no taker redeem, but we are the taker and we have a valid ack. self-revoking. %s", logID)
   403  	match.MetaData.Proof.SelfRevoked = true
   404  }