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 }