decred.org/dcrdex@v1.0.3/client/core/bond.go (about) 1 package core 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "math" 9 "sort" 10 "time" 11 12 "decred.org/dcrdex/client/asset" 13 "decred.org/dcrdex/client/comms" 14 "decred.org/dcrdex/client/db" 15 "decred.org/dcrdex/dex" 16 "decred.org/dcrdex/dex/keygen" 17 "decred.org/dcrdex/dex/msgjson" 18 "decred.org/dcrdex/server/account" 19 "github.com/decred/dcrd/dcrec/secp256k1/v4" 20 "github.com/decred/dcrd/hdkeychain/v3" 21 ) 22 23 const ( 24 // lockTimeLimit is an upper limit on the allowable bond lockTime. 25 lockTimeLimit = 120 * 24 * time.Hour 26 27 defaultBondAsset = 42 // DCR 28 29 maxBondedMult = 4 30 bondTickInterval = 20 * time.Second 31 ) 32 33 func cutBond(bonds []*db.Bond, i int) []*db.Bond { // input slice modified 34 bonds[i] = bonds[len(bonds)-1] 35 bonds[len(bonds)-1] = nil 36 bonds = bonds[:len(bonds)-1] 37 return bonds 38 } 39 40 func (c *Core) triggerBondRotation() { 41 select { 42 case c.rotate <- struct{}{}: 43 default: 44 } 45 } 46 47 func (c *Core) watchBonds(ctx context.Context) { 48 t := time.NewTicker(bondTickInterval) 49 defer t.Stop() 50 51 for { 52 select { 53 case <-ctx.Done(): 54 return 55 case <-t.C: 56 c.rotateBonds(ctx) 57 case <-c.rotate: 58 c.rotateBonds(ctx) 59 } 60 } 61 } 62 63 // Bond lifetime 64 // 65 // t0 t1 t2' t2 t3 t4 66 // |~~~|-----------------------^------|====================|+++| 67 // ~ - = 68 // pending (p) live (l) expired (E) maturing (m) 69 // 70 // t0 = authoring/broadcast 71 // t1 = activation (confirmed and accepted) 72 // t2 = expiry (tier accounting) 73 // t3 = lockTime 74 // t4 = spendable (block medianTime > lockTime) 75 // 76 // E = t3 - t2, *constant* duration, dex.BondExpiry() 77 // p = t1 - t0, *variable*, a random process 78 // m = t4 - t3, *variable*, depends on consensus rules and blocks 79 // e.g. several blocks after lockTime passes 80 // 81 // - bonds may be spent at t4 82 // - bonds must be replaced by t2 i.e. broadcast by some t2' 83 // - perfectly aligning t4 for bond A with t2' for bond B is impossible on 84 // account of the variable durations 85 // - t2-t2' should be greater than a large percent of expected pending 86 // durations (t1-t0), see pendingBuffer 87 // 88 // Here a replacement bond B had a long pending period, and it became active 89 // after bond A expired (too late): 90 // 91 // t0 t1 t2' t2 t3 92 // bond A: |~~~|--------------------------^---|====================| 93 // x 94 // bond B: |~~~~~~|------------------... 95 // 96 // Here the replacement bond was confirmed quickly, but l was too short, 97 // causing it to expire before bond A became spendable: 98 // > renew as bond C 99 // bond A: |~~~|----------------------^-------|====================|++‖~~~~~|---... 100 // ✓ x x 101 // bond B: |~|------------------------|=====... 102 // 103 // Similarly, l could have been long enough to broadcast a replacement in time, 104 // but the pending period could be too long (the second "x"). 105 // 106 // Here the replacement bond was broadcast with enough time to confirm before 107 // the previous bond expired, and the previous bond became spendable in time to 108 // broadcast and confirm another replacement (sustainable): 109 // > renew as bond C 110 // bond A: |~~~|----------------------^-------|====================|++‖~~~~~|---... 111 // ✓ ✓ ✓ 112 // bond B: |~~~~~~|-------------------------------|====... 113 // 114 // Thus, bond rotation without tier drops requires l>E+m+p. For 115 // t3-t0 = p+l+E, this means t3-t0 >= 2(E+p)+m. We will assume the time 116 // from authoring to broadcast is negligible, absorbed into the estimate of the 117 // max pending duration. 118 // 119 // tldr: 120 // - when creating a bond, set lockTime = now + minBondLifetime, where 121 // minBondLifetime = 2*(BondExpiry+pendingBuffer)+spendableDelay 122 // - create a replacement bond at lockTime-BondExpiry-pendingBuffer 123 124 // pendingBuffer gives the duration in seconds prior to reaching bond expiry 125 // (account, not lockTime) after which a new bond should be posted to avoid 126 // account tier falling below target while the replacement bond is pending. The 127 // network is a parameter because expected block times are network-dependent, 128 // and we require short bond lifetimes on simnet. 129 func pendingBuffer(net dex.Network) int64 { 130 switch net { 131 case dex.Mainnet: // unpredictable, so extra large to prevent falling tier 132 return 90 * 60 133 case dex.Testnet: // testnet generally has shorter block times, min diff rules, and vacant blocks 134 return 20 * 60 135 default: // Regtest and Simnet have on-demand blocks 136 return 35 137 } 138 } 139 140 // spendableDelay gives a high estimate in seconds of the duration required for 141 // a bond to become spendable after reaching lockTime. This depends on consensus 142 // rules and block times for an asset. For some assets this could be zero, while 143 // for others like Bitcoin, a time-locked output becomes spendable when the 144 // median of the last 11 blocks is greater than the lockTime. This function 145 // returns a high value to avoid all but extremely rare (but temporary) drops in 146 // tier. NOTE: to minimize bond overlap, an asset.Bonder method could provide 147 // this estimate, but it is still very short relative to the entire bond 148 // lifetime, which is on the order of months. 149 func spendableDelay(net dex.Network) int64 { 150 // We use 3*pendingBuffer as a well-padded estimate of this duration. e.g. 151 // with mainnet, we would use a 90 minute pendingBuffer and a 270 minute 152 // spendableDelay to be robust to periods of slow blocks. 153 return 3 * pendingBuffer(net) 154 } 155 156 // minBondLifetime gives the minimum bond lifetime (a duration from now until 157 // lockTime) that a new bond should use to prevent a tier drop. bondExpiry is in 158 // seconds. 159 func minBondLifetime(net dex.Network, bondExpiry int64) time.Duration { 160 lifeTimeSec := 2*(pendingBuffer(net)+bondExpiry) + spendableDelay(net) // 2*(p+E)+m 161 return time.Second * time.Duration(lifeTimeSec) 162 } 163 164 // sumBondStrengths calculates the total strength of a list of bonds. 165 func sumBondStrengths(bonds []*db.Bond, bondAssets map[uint32]*msgjson.BondAsset) (total int64) { 166 for _, bond := range bonds { 167 if bond.Strength > 0 { // Added with v2 reputation 168 total += int64(bond.Strength) 169 continue 170 } 171 // v1. Gotta hope the server didn't change the bond amount. 172 if ba := bondAssets[bond.AssetID]; ba != nil && ba.Amt > 0 { 173 strength := bond.Amount / ba.Amt 174 total += int64(strength) 175 } 176 } 177 return 178 } 179 180 type dexBondCfg struct { 181 bondAssets map[uint32]*msgjson.BondAsset 182 replaceThresh int64 183 bondExpiry int64 184 lockTimeThresh int64 185 haveConnected bool 186 } 187 188 // updateBondReserves iterates existing accounts and calculates the amount that 189 // should be reserved for bonds for each asset. The wallets reserves are 190 // updated regardless of whether the wallet balance can support it. If 191 // exactly 1 balanceCheckID is provided, the balance will be checked for that 192 // asset, and any insufficiencies will be logged. 193 func (c *Core) updateBondReserves(balanceCheckID ...uint32) { 194 reserves := make(map[uint32][]uint64) 195 processDC := func(dc *dexConnection) { 196 bondAssets, _ := dc.bondAssets() 197 if bondAssets == nil { // reconnect loop may be running 198 return 199 } 200 201 dc.acct.authMtx.RLock() 202 defer dc.acct.authMtx.RUnlock() 203 if dc.acct.targetTier == 0 { 204 return 205 } 206 207 bondAsset := bondAssets[dc.acct.bondAsset] 208 if bondAsset == nil { 209 // Logged at login auth. 210 return 211 } 212 future := c.minBondReserves(dc, bondAsset) 213 reserves[bondAsset.ID] = append(reserves[bondAsset.ID], future) 214 } 215 216 for _, dc := range c.dexConnections() { 217 processDC(dc) 218 } 219 220 for _, w := range c.xcWallets() { 221 bonder, is := w.Wallet.(asset.Bonder) 222 if !is { 223 continue 224 } 225 bondValues, found := reserves[w.AssetID] 226 if !found { 227 // Not selected as a bond asset for any exchanges. 228 bonder.SetBondReserves(0) 229 return 230 } 231 var nominalReserves uint64 232 for _, v := range bondValues { 233 nominalReserves += v 234 } 235 // One bondFeeBuffer for each exchange using this asset as their bond 236 // asset. 237 n := uint64(len(bondValues)) 238 feeReserves := n * bonder.BondsFeeBuffer(c.feeSuggestionAny(w.AssetID)) 239 // Even if reserves are 0, we may still want to reserve fees for 240 // renewing bonds. 241 paddedReserves := nominalReserves + feeReserves 242 if len(balanceCheckID) == 1 && w.AssetID == balanceCheckID[0] && w.connected() { 243 // There are a few paths that request balance checks, and the 244 // cached balance is expected to be up-to-date in them all, with the 245 // only possible exception of ReconfigureWallet -> reReserveFunding, 246 // where an error updating the balance in ReconfigureWallet is only 247 // logged as a warning, for some reason. Either way, worst that 248 // happens in that scenario is log an outdated message. 249 w.mtx.RLock() 250 bal := w.balance 251 w.mtx.RUnlock() 252 avail := bal.Available + bal.BondReserves 253 if avail < paddedReserves { 254 c.log.Warnf("Bond reserves of %d %s exceeds available balance of %d", 255 paddedReserves, unbip(w.AssetID), avail) 256 } 257 } 258 bonder.SetBondReserves(paddedReserves) 259 } 260 } 261 262 // minBondReserves calculates the minimum number of tiers that we need to 263 // reserve funds for. minBondReserveTiers must be called with the authMtx 264 // RLocked. 265 func (c *Core) minBondReserves(dc *dexConnection, bondAsset *BondAsset) uint64 { 266 acct, targetTier := dc.acct, dc.acct.targetTier 267 if targetTier == 0 { 268 return 0 269 } 270 // Keep a list of tuples of [weakTime, bondStrength]. Later, we'll check 271 // these against expired bonds, to see how many tiers we can expect to have 272 // refunded funds available for. 273 activeTiers := make([][2]uint64, 0) 274 dexCfg := dc.config() 275 bondExpiry := dexCfg.BondExpiry 276 pBuffer := uint64(pendingBuffer(c.net)) 277 var tierSum uint64 278 for _, bond := range append(acct.pendingBonds, acct.bonds...) { 279 weakTime := bond.LockTime - bondExpiry - pBuffer 280 ba := dexCfg.BondAssets[dex.BipIDSymbol(bond.AssetID)] 281 if ba == nil { 282 // Bond asset no longer supported. Can't calculate strength. 283 // Consider it strength one. 284 activeTiers = append(activeTiers, [2]uint64{weakTime, 1}) 285 continue 286 } 287 288 tiers := bond.Amount / ba.Amt 289 // We won't count any active bond strength > our tier target. 290 if tiers > targetTier-tierSum { 291 tiers = targetTier - tierSum 292 } 293 tierSum += tiers 294 activeTiers = append(activeTiers, [2]uint64{weakTime, tiers}) 295 if tierSum == targetTier { 296 break 297 } 298 } 299 300 // If our active+pending bonds don't cover our target tier for some reason, 301 // we need to add the missing bond strength. Double-count because we 302 // needed to renew these bonds too. 303 reserveTiers := (targetTier - tierSum) * 2 304 sort.Slice(activeTiers, func(i, j int) bool { 305 return activeTiers[i][0] < activeTiers[j][0] 306 }) 307 sort.Slice(acct.expiredBonds, func(i, j int) bool { // probably already is sorted, but whatever 308 return acct.expiredBonds[i].LockTime < acct.expiredBonds[j].LockTime 309 }) 310 sBuffer := uint64(spendableDelay(c.net)) 311 out: 312 for _, bond := range acct.expiredBonds { 313 if bond.AssetID != bondAsset.ID { 314 continue 315 } 316 strength := bond.Amount / bondAsset.Amt 317 refundableTime := bond.LockTime + sBuffer 318 for i, pair := range activeTiers { 319 weakTime, tiers := pair[0], pair[1] 320 if tiers == 0 { 321 continue 322 } 323 if refundableTime >= weakTime { 324 // Everything is time-sorted. If this bond won't be refunded 325 // in time, none of the others will either. 326 break out 327 } 328 // Modify the activeTiers strengths in-place. Will cause some 329 // extra iteration, but beats the complexity of trying to modify 330 // the slice somehow. 331 if tiers < strength { 332 strength -= tiers 333 activeTiers[i][1] = 0 334 } else { 335 activeTiers[i][1] = tiers - strength 336 // strength = 0 337 break 338 } 339 } 340 } 341 for _, pair := range activeTiers { 342 reserveTiers += pair[1] 343 } 344 return reserveTiers * bondAsset.Amt 345 } 346 347 // dexBondConfig retrieves a dex's configuration related to bonds. 348 func (c *Core) dexBondConfig(dc *dexConnection, now int64) *dexBondCfg { 349 lockTimeThresh := now // in case dex is down, expire (to refund when lock time is passed) 350 var bondExpiry int64 351 bondAssets := make(map[uint32]*msgjson.BondAsset) 352 var haveConnected bool 353 if cfg := dc.config(); cfg != nil { 354 haveConnected = true 355 bondExpiry = int64(cfg.BondExpiry) 356 for symb, ba := range cfg.BondAssets { 357 id, _ := dex.BipSymbolID(symb) 358 bondAssets[id] = ba 359 } 360 lockTimeThresh += bondExpiry // when dex is up, expire sooner according to bondExpiry 361 } 362 replaceThresh := lockTimeThresh + pendingBuffer(c.net) // replace before expiry to avoid tier drop 363 return &dexBondCfg{ 364 bondAssets: bondAssets, 365 replaceThresh: replaceThresh, 366 bondExpiry: bondExpiry, 367 haveConnected: haveConnected, 368 lockTimeThresh: lockTimeThresh, 369 } 370 } 371 372 type dexAcctBondState struct { 373 ExchangeAuth 374 repost []*asset.Bond 375 mustPost int64 // includes toComp 376 toComp int64 377 inBonds uint64 378 } 379 380 // bondStateOfDEX collects all the information needed to determine what 381 // bonds need to be refunded and how much new bonds should be posted. 382 func (c *Core) bondStateOfDEX(dc *dexConnection, bondCfg *dexBondCfg) *dexAcctBondState { 383 dc.acct.authMtx.Lock() 384 defer dc.acct.authMtx.Unlock() 385 386 state := new(dexAcctBondState) 387 weakBonds := make([]*db.Bond, 0, len(dc.acct.bonds)) 388 389 filterExpiredBonds := func(bonds []*db.Bond) (liveBonds []*db.Bond) { 390 for _, bond := range bonds { 391 if int64(bond.LockTime) <= bondCfg.lockTimeThresh { 392 // Often auth, reconnect, or a bondexpired notification will 393 // do this first, but we must also here for refunds when the 394 // DEX host is down or gone. 395 dc.acct.expiredBonds = append(dc.acct.expiredBonds, bond) 396 c.log.Infof("Newly expired bond found: %v (%s)", coinIDString(bond.AssetID, bond.CoinID), unbip(bond.AssetID)) 397 } else { 398 if int64(bond.LockTime) <= bondCfg.replaceThresh { 399 weakBonds = append(weakBonds, bond) // but not yet expired (still live or pending) 400 c.log.Debugf("Soon to expire bond found: %v (%s)", 401 coinIDString(bond.AssetID, bond.CoinID), unbip(bond.AssetID)) 402 } 403 liveBonds = append(liveBonds, bond) 404 } 405 } 406 return liveBonds 407 } 408 409 state.Rep, state.TargetTier, state.EffectiveTier = dc.acct.rep, dc.acct.targetTier, dc.acct.rep.EffectiveTier() 410 state.BondAssetID, state.MaxBondedAmt, state.PenaltyComps = dc.acct.bondAsset, dc.acct.maxBondedAmt, dc.acct.penaltyComps 411 state.inBonds, _ = dc.bondTotalInternal(state.BondAssetID) 412 // Screen the unexpired bonds slices. 413 dc.acct.bonds = filterExpiredBonds(dc.acct.bonds) 414 dc.acct.pendingBonds = filterExpiredBonds(dc.acct.pendingBonds) // possibly expired before confirmed 415 state.PendingStrength = sumBondStrengths(dc.acct.pendingBonds, bondCfg.bondAssets) 416 state.WeakStrength = sumBondStrengths(weakBonds, bondCfg.bondAssets) 417 state.LiveStrength = sumBondStrengths(dc.acct.bonds, bondCfg.bondAssets) // for max bonded check 418 state.PendingBonds = dc.pendingBonds() 419 // Extract the expired bonds. 420 state.ExpiredBonds = make([]*db.Bond, len(dc.acct.expiredBonds)) 421 copy(state.ExpiredBonds, dc.acct.expiredBonds) 422 // Retry postbond for pending bonds that may have failed during 423 // submission after their block waiters triggered. 424 state.repost = make([]*asset.Bond, 0, len(dc.acct.pendingBonds)) 425 for _, bond := range dc.acct.pendingBonds { 426 if bondCfg.haveConnected && !c.waiting(bond.CoinID, bond.AssetID) { 427 c.log.Warnf("Found a pending bond that is not waiting for confirmations. Re-posting: %s (%s)", 428 coinIDString(bond.AssetID, bond.CoinID), unbip(bond.AssetID)) 429 state.repost = append(state.repost, assetBond(bond)) 430 } 431 } 432 433 // Calculate number of bonds increments to post. 434 bondedTier := state.LiveStrength + state.PendingStrength 435 strongBondedTier := bondedTier - state.WeakStrength 436 if uint64(strongBondedTier) < state.TargetTier { 437 state.mustPost += int64(state.TargetTier) - strongBondedTier 438 } else if uint64(strongBondedTier) > state.TargetTier { 439 state.Compensation = strongBondedTier - int64(state.TargetTier) 440 } 441 // Look for penalties to replace. 442 expectedServerTier := state.LiveStrength 443 reportedServerTier := state.Rep.EffectiveTier() 444 if reportedServerTier < expectedServerTier { 445 state.toComp = expectedServerTier - reportedServerTier 446 penaltyCompRemainder := int64(dc.acct.penaltyComps) - state.Compensation 447 if penaltyCompRemainder <= 0 { 448 penaltyCompRemainder = 0 449 } 450 if state.toComp > penaltyCompRemainder { 451 state.toComp = penaltyCompRemainder 452 } 453 } 454 state.mustPost += state.toComp 455 return state 456 } 457 458 func (c *Core) exchangeAuth(dc *dexConnection) *ExchangeAuth { 459 return &c.bondStateOfDEX(dc, c.dexBondConfig(dc, time.Now().Unix())).ExchangeAuth 460 } 461 462 type bondID struct { 463 assetID uint32 464 coinID []byte 465 } 466 467 // refundExpiredBonds refunds expired bonds and returns the list of bonds that 468 // have been refunded and their assetIDs. 469 func (c *Core) refundExpiredBonds(ctx context.Context, acct *dexAccount, cfg *dexBondCfg, state *dexAcctBondState, now int64) (map[uint32]struct{}, int64, error) { 470 spentBonds := make([]*bondID, 0, len(state.ExpiredBonds)) 471 assetIDs := make(map[uint32]struct{}) 472 473 for _, bond := range state.ExpiredBonds { 474 bondIDStr := fmt.Sprintf("%v (%s)", coinIDString(bond.AssetID, bond.CoinID), unbip(bond.AssetID)) 475 if now < int64(bond.LockTime) { 476 ttr := time.Duration(int64(bond.LockTime)-now) * time.Second 477 if ttr < 15*time.Minute || ((ttr/time.Minute)%30 == 0 && (ttr%time.Minute <= bondTickInterval)) { 478 c.log.Debugf("Expired bond %v refundable in about %v.", bondIDStr, ttr) 479 } 480 continue 481 } 482 483 assetID := bond.AssetID 484 wallet, err := c.connectedWallet(assetID) 485 if err != nil { 486 c.log.Errorf("%v wallet not available to refund bond %v: %v", 487 unbip(bond.AssetID), bondIDStr, err) 488 continue 489 } 490 if _, ok := wallet.Wallet.(asset.Bonder); !ok { // will fail in RefundBond, but assert here anyway 491 return nil, 0, fmt.Errorf("Wallet %v is not an asset.Bonder", unbip(bond.AssetID)) 492 } 493 494 expired, err := wallet.LockTimeExpired(ctx, time.Unix(int64(bond.LockTime), 0)) 495 if err != nil { 496 c.log.Errorf("Unable to check if bond %v has expired: %v", bondIDStr, err) 497 continue 498 } 499 if !expired { 500 c.log.Debugf("Expired bond %v with lock time %v not yet refundable according to wallet.", 501 bondIDStr, time.Unix(int64(bond.LockTime), 0)) 502 continue 503 } 504 505 // Here we may either refund or renew the bond depending on target 506 // tier and timing. Direct renewal (refund and post in one) is only 507 // useful if there is insufficient reserves or the client had been 508 // stopped for a while. Normally, a bond becoming spendable will not 509 // coincide with the need to post bond. 510 // 511 // TODO: if mustPost > 0 { wallet.RenewBond(...) } 512 513 // Ensure wallet is unlocked for use below. 514 _, err = wallet.refreshUnlock() 515 if err != nil { 516 c.log.Errorf("failed to unlock bond asset wallet %v: %v", unbip(state.BondAssetID), err) 517 continue 518 } 519 520 // Generate a refund tx paying to an address from the currently 521 // connected wallet, using bond.KeyIndex to create the signed 522 // transaction. The RefundTx is really a backup. 523 var refundCoinStr string 524 var refundVal uint64 525 var bondAlreadySpent bool 526 if bond.KeyIndex == math.MaxUint32 { // invalid/unknown key index fallback (v0 db.Bond, which was never released, or unknown bond from server), also will skirt reserves :/ 527 if len(bond.RefundTx) > 0 { 528 refundCoinID, err := wallet.SendTransaction(bond.RefundTx) 529 if err != nil { 530 c.log.Errorf("Failed to broadcast bond refund txn %x: %v", bond.RefundTx, err) 531 continue 532 } 533 refundCoinStr, _ = asset.DecodeCoinID(bond.AssetID, refundCoinID) 534 } else { // else "Unknown bond reported by server", see result.ActiveBonds in authDEX 535 bondAlreadySpent = true 536 } 537 } else { // expected case -- TODO: remove the math.MaxUint32 sometime after bonds V1 538 priv, err := c.bondKeyIdx(bond.AssetID, bond.KeyIndex) 539 if err != nil { 540 c.log.Errorf("Failed to derive bond private key: %v", err) 541 continue 542 } 543 refundCoin, err := wallet.RefundBond(ctx, bond.Version, bond.CoinID, bond.Data, bond.Amount, priv) 544 priv.Zero() 545 bondAlreadySpent = errors.Is(err, asset.CoinNotFoundError) // or never mined! 546 if err != nil { 547 if errors.Is(err, asset.ErrIncorrectBondKey) { // imported account and app seed is different 548 c.log.Warnf("Private key to spend bond %v is not available. Broadcasting backup refund tx.", bondIDStr) 549 refundCoinID, err := wallet.SendTransaction(bond.RefundTx) 550 if err != nil { 551 c.log.Errorf("Failed to broadcast bond refund txn %x: %v", bond.RefundTx, err) 552 continue 553 } 554 refundCoinStr, _ = asset.DecodeCoinID(bond.AssetID, refundCoinID) 555 } else if !bondAlreadySpent { 556 c.log.Errorf("Failed to generate bond refund tx: %v", err) 557 continue 558 } 559 } else { 560 refundCoinStr, refundVal = refundCoin.String(), refundCoin.Value() 561 } 562 } 563 // RefundBond increases reserves when it spends the bond, adding to 564 // the wallet's balance (available or immature). 565 566 // If the user hasn't already manually refunded the bond, broadcast 567 // the refund txn. Mark it refunded and stop tracking regardless. 568 if bondAlreadySpent { 569 c.log.Warnf("Bond output not found, possibly already spent or never mined! "+ 570 "Marking refunded. Backup refund transaction: %x", bond.RefundTx) 571 } else { 572 subject, details := c.formatDetails(TopicBondRefunded, makeCoinIDToken(bond.CoinID.String(), bond.AssetID), acct.host, 573 makeCoinIDToken(refundCoinStr, bond.AssetID), wallet.amtString(refundVal), wallet.amtString(bond.Amount)) 574 c.notify(newBondRefundNote(TopicBondRefunded, subject, details, db.Success)) 575 } 576 577 err = c.db.BondRefunded(acct.host, assetID, bond.CoinID) 578 if err != nil { // next DB load we'll retry, hit bondAlreadySpent, and store here again 579 c.log.Errorf("Failed to mark bond as refunded: %v", err) 580 } 581 582 spentBonds = append(spentBonds, &bondID{assetID, bond.CoinID}) 583 assetIDs[assetID] = struct{}{} 584 } 585 586 // Remove spentbonds from the dexConnection's expiredBonds list. 587 acct.authMtx.Lock() 588 for _, spentBond := range spentBonds { 589 for i, bond := range acct.expiredBonds { 590 if bond.AssetID == spentBond.assetID && bytes.Equal(bond.CoinID, spentBond.coinID) { 591 acct.expiredBonds = cutBond(acct.expiredBonds, i) 592 break // next spentBond 593 } 594 } 595 } 596 expiredBondsStrength := sumBondStrengths(acct.expiredBonds, cfg.bondAssets) 597 acct.authMtx.Unlock() 598 599 return assetIDs, expiredBondsStrength, nil 600 } 601 602 // repostPendingBonds rebroadcasts all pending bond transactions for a 603 // dexConnection. 604 func (c *Core) repostPendingBonds(dc *dexConnection, cfg *dexBondCfg, state *dexAcctBondState, unlocked bool) { 605 for _, bond := range state.repost { 606 if !unlocked { // can't sign the postbond msg 607 c.log.Warnf("Cannot post pending bond for %v until account is unlocked.", dc.acct.host) 608 continue 609 } 610 // Not dependent on authed - this may be the first bond 611 // (registering) where bondConfirmed does authDEX if needed. 612 if bondAsset, ok := cfg.bondAssets[bond.AssetID]; ok { 613 c.monitorBondConfs(dc, bond, bondAsset.Confs, true) // rebroadcast 614 } else { 615 c.log.Errorf("Asset %v no longer supported by %v for bonds! "+ 616 "Pending bond to refund: %s", 617 unbip(bond.AssetID), dc.acct.host, 618 coinIDString(bond.AssetID, bond.CoinID)) 619 // Or maybe the server config will update again? Hard to know 620 // how to handle this. This really shouldn't happen though. 621 } 622 } 623 } 624 625 // postRequiredBonds posts any required bond increments for a dexConnection. 626 func (c *Core) postRequiredBonds( 627 dc *dexConnection, 628 cfg *dexBondCfg, 629 state *dexAcctBondState, 630 bondAsset *msgjson.BondAsset, 631 wallet *xcWallet, 632 expiredStrength int64, 633 unlocked bool, 634 ) (newlyBonded uint64) { 635 636 if state.TargetTier == 0 || state.mustPost <= 0 || cfg.bondExpiry <= 0 { 637 return 638 } 639 640 c.log.Infof("Gotta post %d bond increments now. Target tier %d, current bonded tier %d (%d weak, %d pending), compensating %d penalties", 641 state.mustPost, state.TargetTier, state.Rep.BondedTier, state.WeakStrength, state.PendingStrength, state.toComp) 642 643 if !unlocked || dc.status() != comms.Connected { 644 c.log.Warnf("Unable to post the required bond while disconnected or account is locked.") 645 return 646 } 647 _, err := wallet.refreshUnlock() 648 if err != nil { 649 c.log.Errorf("failed to unlock bond asset wallet %v: %v", unbip(state.BondAssetID), err) 650 return 651 } 652 err = wallet.checkPeersAndSyncStatus() 653 if err != nil { 654 c.log.Errorf("Cannot post new bonds yet. %v", err) 655 return 656 } 657 658 // For the max bonded limit, we'll normalize all bonds to the 659 // currently selected bond asset. 660 toPost := state.mustPost 661 amt := bondAsset.Amt * uint64(state.mustPost) 662 currentlyBondedAmt := uint64(state.PendingStrength+state.LiveStrength+expiredStrength) * bondAsset.Amt 663 for state.MaxBondedAmt > 0 && amt+currentlyBondedAmt > state.MaxBondedAmt && toPost > 0 { 664 toPost-- // dumber, but reads easier 665 amt = bondAsset.Amt * uint64(toPost) 666 } 667 if toPost == 0 { 668 c.log.Warnf("Unable to post new bond with equivalent of %s currently bonded (limit of %s)", 669 wallet.amtString(currentlyBondedAmt), wallet.amtString(state.MaxBondedAmt)) 670 return 671 } 672 if toPost < state.mustPost { 673 c.log.Warnf("Only posting %d bond increments instead of %d because of current bonding limit of %s", 674 toPost, state.mustPost, wallet.amtString(state.MaxBondedAmt)) 675 } 676 677 lockTime, err := c.calculateMergingLockTime(dc) 678 if err != nil { 679 c.log.Errorf("Error calculating merging locktime: %v", err) 680 return 681 } 682 683 _, err = c.makeAndPostBond(dc, true, wallet, amt, c.feeSuggestionAny(wallet.AssetID), lockTime, bondAsset) 684 if err != nil { 685 c.log.Errorf("Unable to post bond: %v", err) 686 return 687 } 688 return amt 689 } 690 691 // rotateBonds should only be run sequentially i.e. in the watchBonds loop. 692 func (c *Core) rotateBonds(ctx context.Context) { 693 // 1. Refund bonds with passed lockTime. 694 // 2. Move bonds that are expired according to DEX bond expiry into 695 // expiredBonds (lockTime<lockTimeThresh). 696 // 3. Add bonds to keep N bonds active, according to target tier and max 697 // bonded amount, posting before expiry of the bond being replaced. 698 699 if !c.bondKeysReady() { // not logged in, and nextBondKey requires login to decrypt bond xpriv 700 return // nothing to do until wallets are connected on login 701 } 702 703 now := time.Now().Unix() 704 705 for _, dc := range c.dexConnections() { 706 initialized, unlocked := dc.acct.status() 707 if !initialized { 708 continue // view-only or temporary connection 709 } 710 // Account unlocked is generally implied by bondKeysReady, but we will 711 // check per-account before post since accounts can be individually 712 // locked. However, we must refund bonds regardless. 713 714 bondCfg := c.dexBondConfig(dc, now) 715 if len(bondCfg.bondAssets) == 0 && !dc.acct.isDisabled() { 716 if !dc.IsDown() && dc.config() != nil { 717 dc.log.Meter("no-bond-assets", time.Minute*10).Warnf("Zero bond assets reported for apparently connected DCRDEX server") 718 } 719 continue 720 } 721 acctBondState := c.bondStateOfDEX(dc, bondCfg) 722 723 refundedAssets, expiredStrength, err := c.refundExpiredBonds(ctx, dc.acct, bondCfg, acctBondState, now) 724 if err != nil { 725 c.log.Errorf("Failed to refund expired bonds for %v: %v", dc.acct.host, err) 726 continue 727 } 728 for assetID := range refundedAssets { 729 c.updateAssetBalance(assetID) 730 } 731 732 if dc.acct.isDisabled() { 733 continue // For disabled account, we should only bother about unspent bonds that might have been refunded by refundExpiredBonds above. 734 } 735 736 c.repostPendingBonds(dc, bondCfg, acctBondState, unlocked) 737 738 bondAsset := bondCfg.bondAssets[acctBondState.BondAssetID] 739 if bondAsset == nil { 740 if acctBondState.TargetTier > 0 { 741 c.log.Warnf("Bond asset %d not supported by DEX %v", acctBondState.BondAssetID, dc.acct.host) 742 } 743 continue 744 } 745 746 wallet, err := c.connectedWallet(acctBondState.BondAssetID) 747 if err != nil { 748 if acctBondState.TargetTier > 0 { 749 c.log.Errorf("%v wallet not available for bonds: %v", unbip(acctBondState.BondAssetID), err) 750 } 751 continue 752 } 753 754 c.postRequiredBonds(dc, bondCfg, acctBondState, bondAsset, wallet, expiredStrength, unlocked) 755 } 756 757 c.updateBondReserves() 758 } 759 760 func (c *Core) preValidateBond(dc *dexConnection, bond *asset.Bond) error { 761 if len(dc.acct.encKey) == 0 { 762 return fmt.Errorf("uninitialized account") 763 } 764 765 pkBytes := dc.acct.pubKey() 766 if len(pkBytes) == 0 { 767 return fmt.Errorf("account keys not decrypted") 768 } 769 770 // Pre-validate with the raw bytes of the unsigned tx and our account 771 // pubkey. 772 preBond := &msgjson.PreValidateBond{ 773 AcctPubKey: pkBytes, 774 AssetID: bond.AssetID, 775 Version: bond.Version, 776 RawTx: bond.UnsignedTx, 777 } 778 779 preBondRes := new(msgjson.PreValidateBondResult) 780 err := dc.signAndRequest(preBond, msgjson.PreValidateBondRoute, preBondRes, DefaultResponseTimeout) 781 if err != nil { 782 return codedError(registerErr, err) 783 } 784 // Check the response signature. 785 err = dc.acct.checkSig(append(preBondRes.Serialize(), bond.UnsignedTx...), preBondRes.Sig) 786 if err != nil { 787 return newError(signatureErr, "preValidateBond: DEX signature validation error: %v", err) 788 } 789 790 if preBondRes.Amount != bond.Amount { 791 return newError(bondTimeErr, "pre-validated bond amount is not the desired amount: %d != %d", 792 preBondRes.Amount, bond.Amount) 793 } 794 795 return nil 796 } 797 798 func (c *Core) postBond(dc *dexConnection, bond *asset.Bond) (*msgjson.PostBondResult, error) { 799 if len(dc.acct.encKey) == 0 { 800 return nil, fmt.Errorf("uninitialized account") 801 } 802 803 pkBytes := dc.acct.pubKey() 804 if len(pkBytes) == 0 { 805 return nil, fmt.Errorf("account keys not decrypted") 806 } 807 assetID, bondCoin := bond.AssetID, bond.CoinID 808 bondCoinStr := coinIDString(assetID, bondCoin) 809 810 // Do a postbond request with the raw bytes of the unsigned tx, the bond 811 // script, and our account pubkey. 812 postBond := &msgjson.PostBond{ 813 AcctPubKey: pkBytes, 814 AssetID: assetID, 815 Version: bond.Version, 816 CoinID: bondCoin, 817 } 818 819 postBondRes := new(msgjson.PostBondResult) 820 err := dc.signAndRequest(postBond, msgjson.PostBondRoute, postBondRes, DefaultResponseTimeout) 821 if err != nil { 822 return nil, codedError(registerErr, err) 823 } 824 825 // Check the response signature. 826 err = dc.acct.checkSig(postBondRes.Serialize(), postBondRes.Sig) 827 if err != nil { 828 c.log.Warnf("postbond: DEX signature validation error: %v", err) 829 } 830 831 if !bytes.Equal(postBondRes.BondID, bondCoin) { 832 return nil, fmt.Errorf("server reported bond coin ID %v, expected %v", coinIDString(assetID, postBondRes.BondID), 833 bondCoinStr) 834 } 835 836 dc.acct.authMtx.Lock() 837 dc.updateReputation(postBondRes.Reputation) 838 dc.acct.authMtx.Unlock() 839 840 return postBondRes, nil 841 } 842 843 // postAndConfirmBond submits a postbond request for the given bond. 844 func (c *Core) postAndConfirmBond(dc *dexConnection, bond *asset.Bond) { 845 assetID, coinID := bond.AssetID, bond.CoinID 846 coinIDStr := coinIDString(assetID, coinID) 847 848 // Inform the server, which will attempt to locate the bond and check 849 // confirmations. If server sees the required number of confirmations, the 850 // bond will be active (and account created if new) and we should confirm 851 // the bond (in DB and dc.acct.{bond,pendingBonds}). 852 pbr, err := c.postBond(dc, bond) // can be long while server searches 853 if err != nil { 854 subject, details := c.formatDetails(TopicBondPostError, err, err) 855 c.notify(newBondPostNote(TopicBondPostError, subject, details, db.ErrorLevel, dc.acct.host)) 856 return 857 } 858 859 c.log.Infof("Bond confirmed %v (%s) with expire time of %v", coinIDStr, 860 unbip(assetID), time.Unix(int64(pbr.Expiry), 0)) 861 err = c.bondConfirmed(dc, assetID, coinID, pbr) 862 if err != nil { 863 c.log.Errorf("Unable to confirm bond: %v", err) 864 } 865 } 866 867 // monitorBondConfs launches a block waiter for the bond txns to reach the 868 // required amount of confirmations. Once the requirement is met the server is 869 // notified. 870 func (c *Core) monitorBondConfs(dc *dexConnection, bond *asset.Bond, reqConfs uint32, rebroadcast ...bool) { 871 assetID, coinID := bond.AssetID, bond.CoinID 872 coinIDStr := coinIDString(assetID, coinID) 873 host := dc.acct.host 874 875 wallet, err := c.connectedWallet(assetID) 876 if err != nil { 877 c.log.Errorf("No connected wallet for asset %v: %v", unbip(assetID), err) 878 return 879 } 880 lastConfs, err := wallet.RegFeeConfirmations(c.ctx, coinID) 881 coinNotFound := errors.Is(err, asset.CoinNotFoundError) 882 if err != nil && !coinNotFound { 883 c.log.Errorf("Error getting confirmations for %s: %w", coinIDStr, err) 884 return 885 } 886 887 if lastConfs >= reqConfs { // don't bother waiting for a block 888 go c.postAndConfirmBond(dc, bond) 889 return 890 } 891 892 if coinNotFound || (len(rebroadcast) > 0 && rebroadcast[0]) { 893 // Broadcast the bond and start waiting for confs. 894 c.log.Infof("Rebroadcasting bond %v (%s), data = %x.\n\n"+ 895 "BACKUP refund tx paying to current wallet: %x\n\n", 896 coinIDStr, unbip(bond.AssetID), bond.Data, bond.RedeemTx) 897 c.log.Tracef("Raw bond transaction: %x", bond.SignedTx) 898 if _, err = wallet.SendTransaction(bond.SignedTx); err != nil { 899 c.log.Warnf("Failed to broadcast bond txn (%v): Tx bytes %x", err, bond.SignedTx) 900 // TODO: screen inputs if the tx is trying to spend spent outputs 901 // (invalid bond transaction that should be abandoned). 902 } 903 c.updateAssetBalance(bond.AssetID) 904 } 905 906 c.updatePendingBondConfs(dc, bond.AssetID, bond.CoinID, lastConfs) 907 908 trigger := func() (bool, error) { 909 // Retrieve the current wallet in case it was reconfigured. 910 wallet, _ := c.wallet(assetID) // We already know the wallet is there by now. 911 confs, err := wallet.RegFeeConfirmations(c.ctx, coinID) 912 if err != nil && !errors.Is(err, asset.CoinNotFoundError) { 913 return false, fmt.Errorf("Error getting confirmations for %s: %w", coinIDStr, err) 914 } 915 916 if confs != lastConfs { 917 c.updateAssetBalance(assetID) 918 lastConfs = confs 919 c.updatePendingBondConfs(dc, bond.AssetID, bond.CoinID, confs) 920 } 921 922 if confs < reqConfs { 923 details := fmt.Sprintf("Bond confirmations %v/%v", confs, reqConfs) 924 c.notify(newBondPostNoteWithConfirmations(TopicRegUpdate, string(TopicRegUpdate), 925 details, db.Data, assetID, coinIDStr, int32(confs), host, c.exchangeAuth(dc))) 926 } 927 928 return confs >= reqConfs, nil 929 } 930 931 c.wait(coinID, assetID, trigger, func(err error) { 932 if err != nil { 933 subject, details := c.formatDetails(TopicBondPostErrorConfirm, host, err) 934 c.notify(newBondPostNote(TopicBondPostError, subject, details, db.ErrorLevel, host)) 935 return 936 } 937 938 c.log.Infof("DEX %v bond txn %s now has %d confirmations. Submitting postbond request...", 939 host, coinIDStr, reqConfs) 940 941 c.postAndConfirmBond(dc, bond) // if it fails (e.g. timeout), retry in rotateBonds 942 }) 943 } 944 945 // RedeemPrepaidBond redeems a pre-paid bond for a dcrdex host server. 946 func (c *Core) RedeemPrepaidBond(appPW []byte, code []byte, host string, certI any) (tier uint64, err error) { 947 // Make sure the app has been initialized. 948 if !c.IsInitialized() { 949 return 0, fmt.Errorf("app not initialized") 950 } 951 952 // Check the app password. 953 crypter, err := c.encryptionKey(appPW) 954 if err != nil { 955 return 0, codedError(passwordErr, err) 956 } 957 defer crypter.Close() 958 959 var success, acctExists bool 960 961 c.connMtx.RLock() 962 dc, found := c.conns[host] 963 c.connMtx.RUnlock() 964 if found { 965 acctExists = !dc.acct.isViewOnly() 966 if acctExists { 967 if dc.acct.locked() { // require authDEX first to reconcile any existing bond statuses 968 return 0, newError(acctKeyErr, "acct locked %s (login first)", host) 969 } 970 } 971 } else { 972 // New DEX connection. 973 cert, err := parseCert(host, certI, c.net) 974 if err != nil { 975 return 0, newError(fileReadErr, "failed to read certificate file from %s: %v", cert, err) 976 } 977 dc, err = c.connectDEX(&db.AccountInfo{ 978 Host: host, 979 Cert: cert, 980 // bond maintenance options set below. 981 }) 982 if err != nil { 983 return 0, codedError(connectionErr, err) 984 } 985 986 // Close the connection to the dex server if the registration fails. 987 defer func() { 988 if !success { 989 dc.connMaster.Disconnect() 990 } 991 }() 992 } 993 994 if !acctExists { // new dex connection or pre-existing view-only connection 995 _, err := c.discoverAccount(dc, crypter) 996 if err != nil { 997 return 0, err 998 } 999 } 1000 1001 pkBytes := dc.acct.pubKey() 1002 if len(pkBytes) == 0 { 1003 return 0, fmt.Errorf("account keys not decrypted") 1004 } 1005 1006 // Do a postbond request with the raw bytes of the unsigned tx, the bond 1007 // script, and our account pubkey. 1008 postBond := &msgjson.PostBond{ 1009 AcctPubKey: pkBytes, 1010 AssetID: account.PrepaidBondID, 1011 // Version: 0, 1012 CoinID: code, 1013 } 1014 postBondRes := new(msgjson.PostBondResult) 1015 if err = dc.signAndRequest(postBond, msgjson.PostBondRoute, postBondRes, DefaultResponseTimeout); err != nil { 1016 return 0, codedError(registerErr, err) 1017 } 1018 1019 // Check the response signature. 1020 err = dc.acct.checkSig(postBondRes.Serialize(), postBondRes.Sig) 1021 if err != nil { 1022 c.log.Warnf("postbond: DEX signature validation error: %v", err) 1023 } 1024 1025 lockTime := postBondRes.Expiry + dc.config().BondExpiry 1026 1027 dbBond := &db.Bond{ 1028 // Version: 0, 1029 AssetID: account.PrepaidBondID, 1030 CoinID: code, 1031 LockTime: lockTime, 1032 Strength: postBondRes.Strength, 1033 Confirmed: true, 1034 } 1035 1036 dc.acct.authMtx.Lock() 1037 dc.updateReputation(postBondRes.Reputation) 1038 dc.acct.bonds = append(dc.acct.bonds, dbBond) 1039 dc.acct.authMtx.Unlock() 1040 1041 if !acctExists { 1042 dc.acct.keyMtx.RLock() 1043 ai := &db.AccountInfo{ 1044 Host: dc.acct.host, 1045 Cert: dc.acct.cert, 1046 DEXPubKey: dc.acct.dexPubKey, 1047 EncKeyV2: dc.acct.encKey, 1048 Bonds: []*db.Bond{dbBond}, 1049 } 1050 dc.acct.keyMtx.RUnlock() 1051 1052 if err = c.dbCreateOrUpdateAccount(dc, ai); err != nil { 1053 return 0, fmt.Errorf("failed to store pre-paid account for dex %s: %w", host, err) 1054 } 1055 c.addDexConnection(dc) 1056 } 1057 1058 success = true // Don't disconnect anymore. 1059 1060 if err = c.db.AddBond(dc.acct.host, dbBond); err != nil { 1061 return 0, fmt.Errorf("failed to store pre-paid bond for dex %s: %w", host, err) 1062 } 1063 1064 if err = c.bondConfirmed(dc, account.PrepaidBondID, code, postBondRes); err != nil { 1065 return 0, fmt.Errorf("bond redeemed, but failed to auth: %v", err) 1066 } 1067 1068 c.updateBondReserves() 1069 1070 c.notify(newBondAuthUpdate(dc.acct.host, c.exchangeAuth(dc))) 1071 1072 return uint64(postBondRes.Strength), nil 1073 } 1074 1075 func deriveBondKey(bondXPriv *hdkeychain.ExtendedKey, assetID, bondIndex uint32) (*secp256k1.PrivateKey, error) { 1076 kids := []uint32{ 1077 assetID + hdkeychain.HardenedKeyStart, 1078 bondIndex, 1079 } 1080 extKey, err := keygen.GenDeepChildFromXPriv(bondXPriv, kids) 1081 if err != nil { 1082 return nil, fmt.Errorf("GenDeepChild error: %w", err) 1083 } 1084 privB, err := extKey.SerializedPrivKey() 1085 if err != nil { 1086 return nil, fmt.Errorf("SerializedPrivKey error: %w", err) 1087 } 1088 priv := secp256k1.PrivKeyFromBytes(privB) 1089 return priv, nil 1090 } 1091 1092 func deriveBondXPriv(seed []byte) (*hdkeychain.ExtendedKey, error) { 1093 return keygen.GenDeepChild(seed, []uint32{hdKeyPurposeBonds}) 1094 } 1095 1096 func (c *Core) bondKeyIdx(assetID, idx uint32) (*secp256k1.PrivateKey, error) { 1097 c.loginMtx.Lock() 1098 defer c.loginMtx.Unlock() 1099 1100 if c.bondXPriv == nil { 1101 return nil, errors.New("not logged in") 1102 } 1103 1104 return deriveBondKey(c.bondXPriv, assetID, idx) 1105 } 1106 1107 // nextBondKey generates the private key for the next bond, incrementing a 1108 // persistent bond index counter. This method requires login to decrypt and set 1109 // the bond xpriv, so use the bondKeysReady method to ensure it is ready first. 1110 // The bond key index is returned so the same key may be regenerated. 1111 func (c *Core) nextBondKey(assetID uint32) (*secp256k1.PrivateKey, uint32, error) { 1112 nextBondKeyIndex, err := c.db.NextBondKeyIndex(assetID) 1113 if err != nil { 1114 return nil, 0, fmt.Errorf("NextBondIndex: %v", err) 1115 } 1116 1117 priv, err := c.bondKeyIdx(assetID, nextBondKeyIndex) 1118 if err != nil { 1119 return nil, 0, fmt.Errorf("bondKeyIdx: %v", err) 1120 } 1121 return priv, nextBondKeyIndex, nil 1122 } 1123 1124 // UpdateBondOptions sets the bond rotation options for a DEX host, including 1125 // the target trading tier, the preferred asset to use for bonds, and the 1126 // maximum amount allowable to be locked in bonds. 1127 func (c *Core) UpdateBondOptions(form *BondOptionsForm) error { 1128 dc, _, err := c.dex(form.Host) 1129 if err != nil { 1130 return err 1131 } 1132 // TODO: exclude unregistered and/or watch-only 1133 dbAcct, err := c.db.Account(form.Host) 1134 if err != nil { 1135 return err 1136 } 1137 1138 bondAssets, _ := dc.bondAssets() 1139 if bondAssets == nil { 1140 c.log.Warnf("DEX host %v is offline. Bond reconfiguration options are limited to disabling.", 1141 dc.acct.host) 1142 } 1143 1144 // For certain changes, update one or more wallet balances when done. 1145 var tierChanged, assetChanged bool 1146 var wallet *xcWallet // new wallet 1147 var bondAssetID0 uint32 // old wallet's asset ID 1148 var targetTier0, maxBondedAmt0 uint64 1149 var penaltyComps0 uint16 1150 defer func() { 1151 if (tierChanged || assetChanged) && (wallet != nil) { 1152 if _, err := c.updateWalletBalance(wallet); err != nil { 1153 c.log.Errorf("Unable to set balance for wallet %v", wallet.Symbol) 1154 } 1155 if wallet.AssetID != bondAssetID0 && targetTier0 > 0 { 1156 c.updateAssetBalance(bondAssetID0) 1157 } 1158 } 1159 }() 1160 1161 var success bool 1162 dc.acct.authMtx.Lock() 1163 defer func() { 1164 dc.acct.authMtx.Unlock() 1165 if success { 1166 c.notify(newBondAuthUpdate(dc.acct.host, c.exchangeAuth(dc))) 1167 } 1168 }() 1169 1170 if !dc.acct.isAuthed { 1171 return errors.New("login or register first") 1172 } 1173 1174 // Revert to initial values if we encounter any error below. 1175 bondAssetID0 = dc.acct.bondAsset 1176 targetTier0, maxBondedAmt0, penaltyComps0 = dc.acct.targetTier, dc.acct.maxBondedAmt, dc.acct.penaltyComps 1177 defer func() { // still under authMtx lock on defer stack 1178 if !success { 1179 dc.acct.bondAsset = bondAssetID0 1180 dc.acct.maxBondedAmt = maxBondedAmt0 1181 dc.acct.penaltyComps = penaltyComps0 1182 if dc.acct.targetTier > 0 || assetChanged { 1183 dc.acct.targetTier = targetTier0 1184 } // else the user was trying to clear target tier and the wallet was gone too 1185 } 1186 }() 1187 1188 // Verify the new bond asset wallet first. 1189 bondAssetID := bondAssetID0 1190 if form.BondAssetID != nil { 1191 bondAssetID = *form.BondAssetID 1192 } 1193 assetChanged = bondAssetID != bondAssetID0 1194 1195 targetTier := targetTier0 1196 if form.TargetTier != nil { 1197 targetTier = *form.TargetTier 1198 } 1199 tierChanged = targetTier != targetTier0 1200 if tierChanged { 1201 dc.acct.targetTier = targetTier 1202 dbAcct.TargetTier = targetTier 1203 } 1204 1205 var penaltyComps = penaltyComps0 1206 if form.PenaltyComps != nil { 1207 penaltyComps = *form.PenaltyComps 1208 } 1209 dc.acct.penaltyComps = penaltyComps 1210 dbAcct.PenaltyComps = penaltyComps 1211 1212 var bondAssetAmt uint64 // because to disable we must proceed even with no config 1213 bondAsset := bondAssets[bondAssetID] 1214 if bondAsset == nil { 1215 if targetTier > 0 || assetChanged { 1216 return fmt.Errorf("dex %v is does not support %v as a bond asset (or we lack their config)", 1217 dbAcct.Host, unbip(bondAssetID)) 1218 } // else disable, attempting to unreserve funds if wallet is available 1219 } else { 1220 bondAssetAmt = bondAsset.Amt 1221 } 1222 1223 // If we're lowering our bond, we can't set the max bonded amount too low. 1224 tierForDefaultMaxBonded := targetTier 1225 if targetTier > 0 && targetTier0 > targetTier { 1226 tierForDefaultMaxBonded = targetTier0 1227 } 1228 1229 maxBonded := maxBondedMult * bondAssetAmt * (tierForDefaultMaxBonded + uint64(penaltyComps)) // the min if none specified 1230 if form.MaxBondedAmt != nil { 1231 requested := *form.MaxBondedAmt 1232 if requested < maxBonded { 1233 return fmt.Errorf("requested bond maximum of %d is lower than minimum of %d", requested, maxBonded) 1234 } 1235 maxBonded = requested 1236 } 1237 1238 var found bool 1239 wallet, found = c.wallet(bondAssetID) 1240 if !found || !wallet.connected() { 1241 return fmt.Errorf("bond asset wallet %v does not exist or is not connected", unbip(bondAssetID)) 1242 } 1243 bonder, ok := wallet.Wallet.(asset.Bonder) 1244 if !ok { 1245 return fmt.Errorf("wallet %v is not an asset.Bonder", unbip(bondAssetID)) 1246 } 1247 1248 _, err = wallet.refreshUnlock() 1249 if err != nil { 1250 return fmt.Errorf("bond asset wallet %v is locked", unbip(bondAssetID)) 1251 } 1252 1253 if assetChanged || tierChanged { 1254 bal, err := wallet.Balance() 1255 if err != nil { 1256 return fmt.Errorf("failed to get balance for %s wallet: %w", unbip(bondAssetID), err) 1257 } 1258 avail := bal.Available + bal.BondReserves 1259 1260 // We need to recalculate bond reserves, including all other exchanges. 1261 // We're under the dc.acct.authMtx lock, so we'll add our contribution 1262 // first and then iterate the others in a loop where we're okay to lock 1263 // their authMtx (via bondTotal). 1264 nominalReserves := c.minBondReserves(dc, bondAsset) 1265 var n uint64 1266 if targetTier > 0 { 1267 n = 1 1268 } 1269 var tiers uint64 = targetTier 1270 for _, otherDC := range c.dexConnections() { 1271 if otherDC.acct.host == dc.acct.host { // Only adding others 1272 continue 1273 } 1274 assetID, _, _ := otherDC.bondOpts() 1275 if assetID != bondAssetID { 1276 continue 1277 } 1278 bondAsset, _ := otherDC.bondAsset(assetID) 1279 if bondAsset == nil { 1280 continue 1281 } 1282 n++ 1283 tiers += targetTier 1284 ba := BondAsset(*bondAsset) 1285 otherDC.acct.authMtx.RLock() 1286 nominalReserves += c.minBondReserves(dc, &ba) 1287 otherDC.acct.authMtx.RUnlock() 1288 } 1289 1290 var feeReserves uint64 1291 if n > 0 { 1292 feeBuffer := bonder.BondsFeeBuffer(c.feeSuggestionAny(bondAssetID)) 1293 feeReserves = n * feeBuffer 1294 req := nominalReserves + feeReserves 1295 c.log.Infof("%d DEX server(s) using %s for bonding a total of %d tiers. %d required includes %d in fee reserves. Current balance = %d", 1296 n, unbip(bondAssetID), tiers, req, feeReserves, avail) 1297 // If raising the tier or changing asset, enforce available funds. 1298 if (assetChanged || targetTier > targetTier0) && req > avail { 1299 return fmt.Errorf("insufficient funds. need %d, have %d", req, avail) 1300 } 1301 } 1302 1303 bonder.SetBondReserves(nominalReserves + feeReserves) 1304 1305 dc.acct.bondAsset = bondAssetID 1306 dbAcct.BondAsset = bondAssetID 1307 } 1308 1309 if assetChanged || tierChanged || form.MaxBondedAmt != nil || maxBonded < dc.acct.maxBondedAmt { 1310 dc.acct.maxBondedAmt = maxBonded 1311 dbAcct.MaxBondedAmt = maxBonded 1312 } 1313 1314 c.triggerBondRotation() 1315 1316 c.log.Debugf("Bond options for %v: target tier %d, bond asset %d, maxBonded %v", 1317 dbAcct.Host, dc.acct.targetTier, dc.acct.bondAsset, dbAcct.MaxBondedAmt) 1318 1319 if err = c.db.UpdateAccountInfo(dbAcct); err == nil { 1320 success = true 1321 } // else we might have already done ReserveBondFunds... 1322 return err 1323 1324 } 1325 1326 // BondsFeeBuffer suggests how much extra may be required for the transaction 1327 // fees part of bond reserves when bond rotation is enabled. This may be used to 1328 // inform the consumer how much extra (beyond double the bond amount) is 1329 // required to facilitate uninterrupted maintenance of a target trading tier. 1330 func (c *Core) BondsFeeBuffer(assetID uint32) (uint64, error) { 1331 wallet, err := c.connectedWallet(assetID) 1332 if err != nil { 1333 return 0, err 1334 } 1335 bonder, ok := wallet.Wallet.(asset.Bonder) 1336 if !ok { 1337 return 0, errors.New("wallet does not support bonds") 1338 } 1339 return bonder.BondsFeeBuffer(c.feeSuggestionAny(assetID)), nil 1340 } 1341 1342 // PostBond begins the process of posting a new bond for a new or existing DEX 1343 // account. On return, the bond transaction will have been broadcast, and when 1344 // the required number of confirmations is reached, Core will submit the bond 1345 // for acceptance to the server. A TopicBondConfirmed is emitted when the 1346 // fully-confirmed bond is accepted. Before the transaction is broadcasted, a 1347 // prevalidatebond request is sent to ensure the transaction is compliant and 1348 // (and that the intended server is actually online!). PostBond may be used to 1349 // create a new account with a bond, or to top-up bond on an existing account. 1350 // If the account is not yet configured in Core, account discovery will be 1351 // performed prior to posting a new bond. If account discovery finds an existing 1352 // account, the connection is established but no additional bond is posted. If 1353 // no account is discovered on the server, the account is created locally and 1354 // bond is posted to create the account. 1355 // 1356 // Note that the FeeBuffer field of the form is optional, but it may be provided 1357 // to ensure that the wallet reserves the amount reported by a preceding call to 1358 // BondsFeeBuffer, such as during initial wallet funding. 1359 func (c *Core) PostBond(form *PostBondForm) (*PostBondResult, error) { 1360 // Make sure the app has been initialized. 1361 if !c.IsInitialized() { 1362 return nil, fmt.Errorf("app not initialized") 1363 } 1364 1365 // Check that the bond amount is non-zero before we touch wallets and make 1366 // connections to the DEX host. 1367 if form.Bond == 0 { 1368 return nil, newError(bondAmtErr, "zero registration fees not allowed") 1369 } 1370 1371 // Get the wallet to author the transaction. Default to DCR. 1372 bondAssetID := uint32(42) 1373 if form.Asset != nil { 1374 bondAssetID = *form.Asset 1375 } 1376 bondAssetSymbol := dex.BipIDSymbol(bondAssetID) 1377 wallet, err := c.connectedWallet(bondAssetID) 1378 if err != nil { 1379 return nil, fmt.Errorf("cannot connect to %s wallet to pay fee: %w", bondAssetSymbol, err) 1380 } 1381 if _, ok := wallet.Wallet.(asset.Bonder); !ok { // will fail in MakeBondTx, but assert early 1382 return nil, fmt.Errorf("wallet %v is not an asset.Bonder", bondAssetSymbol) 1383 } 1384 err = wallet.checkPeersAndSyncStatus() 1385 if err != nil { 1386 return nil, err 1387 } 1388 1389 // Check the app password. 1390 crypter, err := c.encryptionKey(form.AppPass) 1391 if err != nil { 1392 return nil, codedError(passwordErr, err) 1393 } 1394 defer crypter.Close() 1395 if form.Addr == "" { 1396 return nil, newError(emptyHostErr, "no dex address specified") 1397 } 1398 host, err := addrHost(form.Addr) 1399 if err != nil { 1400 return nil, newError(addressParseErr, "error parsing address: %v", err) 1401 } 1402 1403 // Get ready to generate the bond txn. 1404 if !wallet.unlocked() { 1405 err = wallet.Unlock(crypter) 1406 if err != nil { 1407 return nil, newError(walletAuthErr, "failed to unlock %s wallet: %v", unbip(wallet.AssetID), err) 1408 } 1409 } 1410 1411 var success, acctExists bool 1412 1413 // When creating an account or registering a view-only account, the default 1414 // is to maintain tier. 1415 maintain := true 1416 if form.MaintainTier != nil { 1417 maintain = *form.MaintainTier 1418 } 1419 1420 c.connMtx.RLock() 1421 dc, found := c.conns[host] 1422 c.connMtx.RUnlock() 1423 if found { 1424 acctExists = !dc.acct.isViewOnly() 1425 if acctExists { 1426 if dc.acct.locked() { // require authDEX first to reconcile any existing bond statuses 1427 return nil, newError(acctKeyErr, "acct locked %s (login first)", form.Addr) 1428 } 1429 if form.MaintainTier != nil || form.MaxBondedAmt != nil { 1430 return nil, fmt.Errorf("maintain tier and max bonded amount may only be set when registering " + 1431 "(use UpdateBondOptions to change bond maintenance settings)") 1432 } 1433 } 1434 } else { 1435 // Before connecting to the DEX host, do a quick balance check to ensure 1436 // we at least have the nominal bond amount available. 1437 if bal, err := wallet.Balance(); err != nil { 1438 return nil, newError(bondAssetErr, "unable to check wallet balance: %w", err) 1439 } else if bal.Available < form.Bond { 1440 return nil, newError(walletBalanceErr, "insufficient available balance") 1441 } 1442 1443 // New DEX connection. 1444 cert, err := parseCert(host, form.Cert, c.net) 1445 if err != nil { 1446 return nil, newError(fileReadErr, "failed to read certificate file from %s: %v", cert, err) 1447 } 1448 dc, err = c.connectDEX(&db.AccountInfo{ 1449 Host: host, 1450 Cert: cert, 1451 // bond maintenance options set below. 1452 }) 1453 if err != nil { 1454 return nil, codedError(connectionErr, err) 1455 } 1456 1457 // Close the connection to the dex server if the registration fails. 1458 defer func() { 1459 if !success { 1460 dc.connMaster.Disconnect() 1461 } 1462 }() 1463 } 1464 1465 if !acctExists { // new dex connection or pre-existing view-only connection 1466 paid, err := c.discoverAccount(dc, crypter) 1467 if err != nil { 1468 return nil, err 1469 } 1470 // dc.acct is now configured with encKey, privKey, and id for a new 1471 // (unregistered) account. 1472 if paid { 1473 success = true 1474 // The listen goroutine is already running, now track the conn. 1475 c.addDexConnection(dc) 1476 return &PostBondResult{ /* no new bond */ }, nil 1477 } 1478 } 1479 1480 feeRate := c.feeSuggestionAny(bondAssetID, dc) 1481 1482 // Ensure this DEX supports this asset for bond, and get the required 1483 // confirmations and bond amount. 1484 bondAsset, _ := dc.bondAsset(bondAssetID) 1485 if bondAsset == nil { 1486 return nil, newError(assetSupportErr, "dex host has not connected or does not support fidelity bonds in asset %q", bondAssetSymbol) 1487 } 1488 1489 var lockTime time.Time 1490 if form.LockTime > 0 { 1491 lockTime = time.Unix(int64(form.LockTime), 0) 1492 } else { 1493 lockTime, err = c.calculateMergingLockTime(dc) 1494 if err != nil { 1495 return nil, err 1496 } 1497 } 1498 1499 // Check that the bond amount matches the caller's expectations. 1500 if form.Bond < bondAsset.Amt { 1501 return nil, newError(bondAmtErr, "specified bond amount is less than the DEX-provided amount. %d < %d", 1502 form.Bond, bondAsset.Amt) 1503 } 1504 if rem := form.Bond % bondAsset.Amt; rem != 0 { 1505 return nil, newError(bondAmtErr, "specified bond amount is not a multiple of the DEX-provided amount. %d %% %d = %d", 1506 form.Bond, bondAsset.Amt, rem) 1507 } 1508 if acctExists { // if account exists, advise using UpdateBondOptions 1509 autoBondAsset, targetTier, maxBondedAmt := dc.bondOpts() 1510 c.log.Warnf("Manually posting bond for existing account "+ 1511 "(target tier %d, bond asset %d, maxBonded %v). "+ 1512 "Consider using UpdateBondOptions instead.", 1513 targetTier, autoBondAsset, wallet.amtString(maxBondedAmt)) 1514 } else if maintain { // new account (or registering a view-only acct) with tier maintenance enabled 1515 // Fully pre-reserve funding with the wallet before making and 1516 // transactions. bondConfirmed will call authDEX, which will recognize 1517 // that it is the first authorization of the account with the DEX via 1518 // the totalReserves and isAuthed fields of dexAccount. 1519 maxBondedAmt := maxBondedMult * form.Bond // default 1520 if form.MaxBondedAmt != nil { 1521 maxBondedAmt = *form.MaxBondedAmt 1522 } 1523 dc.acct.authMtx.Lock() 1524 dc.acct.bondAsset = bondAssetID 1525 dc.acct.targetTier = form.Bond / bondAsset.Amt 1526 dc.acct.maxBondedAmt = maxBondedAmt 1527 dc.acct.authMtx.Unlock() 1528 } 1529 1530 // Make a bond transaction for the account ID generated from our public key. 1531 bondCoin, err := c.makeAndPostBond(dc, acctExists, wallet, form.Bond, feeRate, lockTime, bondAsset) 1532 if err != nil { 1533 return nil, err 1534 } 1535 c.updateBondReserves() // Can probably reduce reserves because of the pending bond. 1536 success = true 1537 bondCoinStr := coinIDString(bondAssetID, bondCoin) 1538 return &PostBondResult{BondID: bondCoinStr, ReqConfirms: uint16(bondAsset.Confs)}, nil 1539 } 1540 1541 // calculateMergingLockTime calculates a locktime for a new bond for the 1542 // specified account, with consideration for merging parallel bond tracks. 1543 // Tracks are merged by choosing the locktime of an existing bond if one exists 1544 // and has a locktime value in an acceptable range. We will merge tracks even if 1545 // it means reducing the live period associated with the bond by as much as 1546 // ~75%. 1547 func (c *Core) calculateMergingLockTime(dc *dexConnection) (time.Time, error) { 1548 bondExpiry := int64(dc.config().BondExpiry) 1549 lockDur := minBondLifetime(c.net, bondExpiry) 1550 lockTime := time.Now().Add(lockDur).Truncate(time.Second) 1551 expireTime := lockTime.Add(time.Second * time.Duration(-bondExpiry)) // when the server would expire the bond 1552 if time.Until(expireTime) < time.Minute { 1553 return time.Time{}, newError(bondTimeErr, "bond would expire in less than one minute") 1554 } 1555 if lockDur := time.Until(lockTime); lockDur > lockTimeLimit { 1556 return time.Time{}, newError(bondTimeErr, "excessive lock time (%v>%v)", lockDur, lockTimeLimit) 1557 } 1558 1559 // If we have parallel bond tracks out of sync, we may use an earlier lock 1560 // time in order to get back in sync. 1561 mergeableLocktimeThresh := uint64(time.Now().Unix() + bondExpiry*5/4 + pendingBuffer(c.net)) 1562 var bestMergeableLocktime uint64 1563 dc.acct.authMtx.RLock() 1564 for _, b := range dc.acct.bonds { 1565 if b.LockTime > mergeableLocktimeThresh && (bestMergeableLocktime == 0 || b.LockTime > bestMergeableLocktime) { 1566 bestMergeableLocktime = b.LockTime 1567 } 1568 } 1569 dc.acct.authMtx.RUnlock() 1570 if bestMergeableLocktime > 0 { 1571 newLockTime := time.Unix(int64(bestMergeableLocktime), 0) 1572 bondExpiryDur := time.Duration(bondExpiry) * time.Second 1573 c.log.Infof("Reducing bond expiration date from %s to %s to facilitate merge with parallel bond track", 1574 lockTime.Add(-bondExpiryDur), newLockTime.Add(-bondExpiryDur)) 1575 lockTime = newLockTime 1576 } 1577 return lockTime, nil 1578 } 1579 1580 func (c *Core) makeAndPostBond(dc *dexConnection, acctExists bool, wallet *xcWallet, amt, feeRate uint64, 1581 lockTime time.Time, bondAsset *msgjson.BondAsset) ([]byte, error) { 1582 1583 bondKey, keyIndex, err := c.nextBondKey(bondAsset.ID) 1584 if err != nil { 1585 return nil, fmt.Errorf("bond key derivation failed: %v", err) 1586 } 1587 defer bondKey.Zero() 1588 1589 acctID := dc.acct.ID() 1590 bond, abandon, err := wallet.MakeBondTx(bondAsset.Version, amt, feeRate, lockTime, bondKey, acctID[:]) 1591 if err != nil { 1592 return nil, codedError(bondPostErr, err) 1593 } 1594 // MakeBondTx lock coins and reduces reserves in proportion 1595 1596 var success bool 1597 defer func() { 1598 if !success { 1599 abandon() // unlock coins and increase reserves 1600 } 1601 }() 1602 1603 // Do prevalidatebond with the *unsigned* txn. 1604 if err = c.preValidateBond(dc, bond); err != nil { 1605 return nil, err 1606 } 1607 1608 reqConfs := bondAsset.Confs 1609 bondCoinStr := coinIDString(bond.AssetID, bond.CoinID) 1610 c.log.Infof("DEX %v has validated our bond %v (%s) with strength %d. %d confirmations required to trade.", 1611 dc.acct.host, bondCoinStr, unbip(bond.AssetID), amt/bondAsset.Amt, reqConfs) 1612 1613 // Store the account and bond info. 1614 dbBond := &db.Bond{ 1615 Version: bond.Version, 1616 AssetID: bond.AssetID, 1617 CoinID: bond.CoinID, 1618 UnsignedTx: bond.UnsignedTx, 1619 SignedTx: bond.SignedTx, 1620 Data: bond.Data, 1621 Amount: amt, 1622 LockTime: uint64(lockTime.Unix()), 1623 KeyIndex: keyIndex, 1624 RefundTx: bond.RedeemTx, 1625 Strength: uint32(amt / bondAsset.Amt), 1626 // Confirmed and Refunded are false (new bond tx) 1627 } 1628 1629 if acctExists { 1630 err = c.db.AddBond(dc.acct.host, dbBond) 1631 if err != nil { 1632 return nil, fmt.Errorf("failed to store bond %v (%s) for dex %v: %w", 1633 bondCoinStr, unbip(bond.AssetID), dc.acct.host, err) 1634 } 1635 } else { 1636 bondAsset, targetTier, maxBondedAmt := dc.bondOpts() 1637 ai := &db.AccountInfo{ 1638 Host: dc.acct.host, 1639 Cert: dc.acct.cert, 1640 DEXPubKey: dc.acct.dexPubKey, 1641 EncKeyV2: dc.acct.encKey, 1642 Bonds: []*db.Bond{dbBond}, 1643 TargetTier: targetTier, 1644 MaxBondedAmt: maxBondedAmt, 1645 BondAsset: bondAsset, 1646 } 1647 err = c.dbCreateOrUpdateAccount(dc, ai) 1648 if err != nil { 1649 return nil, fmt.Errorf("failed to store account %v for dex %v: %w", 1650 dc.acct.id, dc.acct.host, err) 1651 } 1652 } 1653 1654 success = true // we're doing this 1655 1656 dc.acct.authMtx.Lock() 1657 dc.acct.pendingBonds = append(dc.acct.pendingBonds, dbBond) 1658 dc.acct.authMtx.Unlock() 1659 1660 if !acctExists { // *after* setting pendingBonds for rotateBonds accounting if targetTier>0 1661 c.addDexConnection(dc) 1662 // NOTE: it's still not authed if this was the first bond 1663 } 1664 1665 // Broadcast the bond and start waiting for confs. 1666 c.log.Infof("Broadcasting bond %v (%s) with lock time %v, data = %x.\n\n"+ 1667 "BACKUP refund tx paying to current wallet: %x\n\n", 1668 bondCoinStr, unbip(bond.AssetID), lockTime, bond.Data, bond.RedeemTx) 1669 if bondCoinCast, err := wallet.SendTransaction(bond.SignedTx); err != nil { 1670 c.log.Warnf("Failed to broadcast bond txn (%v). Tx bytes: %x", err, bond.SignedTx) 1671 // There is a good possibility it actually made it to the network. We 1672 // should start monitoring, perhaps even rebroadcast. It's tempting to 1673 // abort and remove the pending bond, but that's bad if it's sent. 1674 } else if !bytes.Equal(bond.CoinID, bondCoinCast) { 1675 c.log.Warnf("Broadcasted bond %v; was expecting %v!", 1676 coinIDString(bond.AssetID, bondCoinCast), bondCoinStr) 1677 } 1678 1679 // Set up the coin waiter, which watches confirmations so the user knows 1680 // when to expect their account to be marked paid by the server. 1681 c.monitorBondConfs(dc, bond, reqConfs) 1682 1683 c.updateAssetBalance(bond.AssetID) 1684 1685 // Start waiting for reqConfs. 1686 subject, details := c.formatDetails(TopicBondConfirming, reqConfs, makeCoinIDToken(bondCoinStr, bond.AssetID), unbip(bond.AssetID), dc.acct.host) 1687 c.notify(newBondPostNoteWithConfirmations(TopicBondConfirming, subject, 1688 details, db.Success, bond.AssetID, bondCoinStr, 0, dc.acct.host, c.exchangeAuth(dc))) 1689 1690 return bond.CoinID, nil 1691 } 1692 1693 func (c *Core) updatePendingBondConfs(dc *dexConnection, assetID uint32, coinID []byte, confs uint32) { 1694 dc.acct.authMtx.Lock() 1695 defer dc.acct.authMtx.Unlock() 1696 bondIDStr := coinIDString(assetID, coinID) 1697 dc.acct.pendingBondsConfs[bondIDStr] = confs 1698 } 1699 1700 func (c *Core) bondConfirmed(dc *dexConnection, assetID uint32, coinID []byte, pbr *msgjson.PostBondResult) error { 1701 bondIDStr := coinIDString(assetID, coinID) 1702 // Update dc.acct.{bonds,pendingBonds,tier} under authMtx lock. 1703 var foundPending, foundConfirmed bool 1704 dc.acct.authMtx.Lock() 1705 delete(dc.acct.pendingBondsConfs, bondIDStr) 1706 for i, bond := range dc.acct.pendingBonds { 1707 if bond.AssetID == assetID && bytes.Equal(bond.CoinID, coinID) { 1708 // Delete the bond from pendingBonds and move it to (active) bonds. 1709 dc.acct.pendingBonds = cutBond(dc.acct.pendingBonds, i) 1710 dc.acct.bonds = append(dc.acct.bonds, bond) 1711 bond.Confirmed = true // not necessary, just for consistency with slice membership 1712 foundPending = true 1713 break 1714 } 1715 } 1716 if !foundPending { 1717 for _, bond := range dc.acct.bonds { 1718 if bond.AssetID == assetID && bytes.Equal(bond.CoinID, coinID) { 1719 foundConfirmed = true 1720 break 1721 } 1722 } 1723 } 1724 1725 dc.acct.rep = *pbr.Reputation 1726 effectiveTier := dc.acct.rep.EffectiveTier() 1727 bondedTier := dc.acct.rep.BondedTier 1728 targetTier := dc.acct.targetTier 1729 isAuthed := dc.acct.isAuthed 1730 dc.acct.authMtx.Unlock() 1731 1732 if foundPending { 1733 // Set bond confirmed in the DB. 1734 err := c.db.ConfirmBond(dc.acct.host, assetID, coinID) 1735 if err != nil { 1736 return fmt.Errorf("db.ConfirmBond failure: %w", err) 1737 } 1738 subject, details := c.formatDetails(TopicBondConfirmed, effectiveTier, targetTier) 1739 c.notify(newBondPostNoteWithTier(TopicBondConfirmed, subject, details, db.Success, dc.acct.host, bondedTier, c.exchangeAuth(dc))) 1740 } else if !foundConfirmed { 1741 c.log.Errorf("bondConfirmed: Bond %s (%s) not found", bondIDStr, unbip(assetID)) 1742 // just try to authenticate... 1743 } // else already found confirmed (no-op) 1744 1745 // If we were not previously authenticated, we can infer that this was the 1746 // bond that created the account server-side, otherwise this was a top-up. 1747 if isAuthed { 1748 return nil // already logged in 1749 } 1750 1751 if dc.acct.locked() { 1752 c.log.Info("Login to check current account tier with newly confirmed bond %v.", bondIDStr) 1753 return nil 1754 } 1755 1756 err := c.authDEX(dc) 1757 if err != nil { 1758 subject, details := c.formatDetails(TopicDexAuthErrorBond, err) 1759 c.notify(newDEXAuthNote(TopicDexAuthError, subject, dc.acct.host, false, details, db.ErrorLevel)) 1760 return err 1761 } 1762 1763 subject, details := c.formatDetails(TopicAccountRegTier, effectiveTier) 1764 c.notify(newBondPostNoteWithTier(TopicAccountRegistered, subject, 1765 details, db.Success, dc.acct.host, bondedTier, c.exchangeAuth(dc))) // possibly redundant with SubjectBondConfirmed 1766 1767 return nil 1768 } 1769 1770 func (c *Core) bondExpired(dc *dexConnection, assetID uint32, coinID []byte, note *msgjson.BondExpiredNotification) error { 1771 // Update dc.acct.{bonds,tier} under authMtx lock. 1772 var found bool 1773 dc.acct.authMtx.Lock() 1774 for i, bond := range dc.acct.bonds { 1775 if bond.AssetID == assetID && bytes.Equal(bond.CoinID, coinID) { 1776 // Delete the bond from bonds and move it to expiredBonds. 1777 dc.acct.bonds = cutBond(dc.acct.bonds, i) 1778 if len(bond.RefundTx) > 0 || bond.KeyIndex != math.MaxUint32 { 1779 dc.acct.expiredBonds = append(dc.acct.expiredBonds, bond) // we'll wait for lockTime to pass to refund 1780 } else { 1781 c.log.Warnf("Dropping expired bond with no known keys or refund transaction. "+ 1782 "This was a placeholder for an unknown bond reported to use by the server. "+ 1783 "Bond ID: %x (%s)", coinIDString(bond.AssetID, bond.CoinID), unbip(bond.AssetID)) 1784 } 1785 found = true 1786 break 1787 } 1788 } 1789 if !found { // rotateBonds may have gotten to it first 1790 for _, bond := range dc.acct.expiredBonds { 1791 if bond.AssetID == assetID && bytes.Equal(bond.CoinID, coinID) { 1792 found = true 1793 break 1794 } 1795 } 1796 } 1797 1798 if note.Reputation != nil { 1799 dc.acct.rep = *note.Reputation 1800 } else { 1801 dc.acct.rep.BondedTier = note.Tier + int64(dc.acct.rep.Penalties) 1802 } 1803 targetTier := dc.acct.targetTier 1804 effectiveTier := dc.acct.rep.EffectiveTier() 1805 bondedTier := dc.acct.rep.BondedTier 1806 dc.acct.authMtx.Unlock() 1807 1808 bondIDStr := coinIDString(assetID, coinID) 1809 if !found { 1810 c.log.Warnf("bondExpired: Bond %s (%s) in bondexpired message not found locally (already refunded?).", 1811 bondIDStr, unbip(assetID)) 1812 } 1813 1814 if int64(targetTier) > effectiveTier { 1815 subject, details := c.formatDetails(TopicBondExpired, effectiveTier, targetTier) 1816 c.notify(newBondPostNoteWithTier(TopicBondExpired, subject, 1817 details, db.WarningLevel, dc.acct.host, bondedTier, c.exchangeAuth(dc))) 1818 } 1819 1820 return nil 1821 }