decred.org/dcrdex@v1.0.5/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 485 if assetID == account.PrepaidBondID { 486 if err := c.db.BondRefunded(acct.host, assetID, bond.CoinID); err != nil { 487 c.log.Errorf("Failed to mark pre-paid bond as refunded: %v", err) 488 } else { 489 spentBonds = append(spentBonds, &bondID{assetID, bond.CoinID}) 490 } 491 continue 492 } 493 494 wallet, err := c.connectedWallet(assetID) 495 if err != nil { 496 c.log.Errorf("%v wallet not available to refund bond %v: %v", 497 unbip(bond.AssetID), bondIDStr, err) 498 continue 499 } 500 if _, ok := wallet.Wallet.(asset.Bonder); !ok { // will fail in RefundBond, but assert here anyway 501 return nil, 0, fmt.Errorf("wallet %v is not an asset.Bonder", unbip(bond.AssetID)) 502 } 503 504 expired, err := wallet.LockTimeExpired(ctx, time.Unix(int64(bond.LockTime), 0)) 505 if err != nil { 506 c.log.Errorf("Unable to check if bond %v has expired: %v", bondIDStr, err) 507 continue 508 } 509 if !expired { 510 c.log.Debugf("Expired bond %v with lock time %v not yet refundable according to wallet.", 511 bondIDStr, time.Unix(int64(bond.LockTime), 0)) 512 continue 513 } 514 515 // Here we may either refund or renew the bond depending on target 516 // tier and timing. Direct renewal (refund and post in one) is only 517 // useful if there is insufficient reserves or the client had been 518 // stopped for a while. Normally, a bond becoming spendable will not 519 // coincide with the need to post bond. 520 // 521 // TODO: if mustPost > 0 { wallet.RenewBond(...) } 522 523 // Ensure wallet is unlocked for use below. 524 _, err = wallet.refreshUnlock() 525 if err != nil { 526 c.log.Errorf("failed to unlock bond asset wallet %v: %v", unbip(state.BondAssetID), err) 527 continue 528 } 529 530 // Generate a refund tx paying to an address from the currently 531 // connected wallet, using bond.KeyIndex to create the signed 532 // transaction. The RefundTx is really a backup. 533 var refundCoinStr string 534 var refundVal uint64 535 var bondAlreadySpent bool 536 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 :/ 537 if len(bond.RefundTx) > 0 { 538 refundCoinID, err := wallet.SendTransaction(bond.RefundTx) 539 if err != nil { 540 c.log.Errorf("Failed to broadcast bond refund txn %x: %v", bond.RefundTx, err) 541 continue 542 } 543 refundCoinStr, _ = asset.DecodeCoinID(bond.AssetID, refundCoinID) 544 } else { // else "Unknown bond reported by server", see result.ActiveBonds in authDEX 545 bondAlreadySpent = true 546 } 547 } else { // expected case -- TODO: remove the math.MaxUint32 sometime after bonds V1 548 priv, err := c.bondKeyIdx(bond.AssetID, bond.KeyIndex) 549 if err != nil { 550 c.log.Errorf("Failed to derive bond private key: %v", err) 551 continue 552 } 553 refundCoin, err := wallet.RefundBond(ctx, bond.Version, bond.CoinID, bond.Data, bond.Amount, priv) 554 priv.Zero() 555 bondAlreadySpent = errors.Is(err, asset.CoinNotFoundError) // or never mined! 556 if err != nil { 557 if errors.Is(err, asset.ErrIncorrectBondKey) { // imported account and app seed is different 558 c.log.Warnf("Private key to spend bond %v is not available. Broadcasting backup refund tx.", bondIDStr) 559 refundCoinID, err := wallet.SendTransaction(bond.RefundTx) 560 if err != nil { 561 c.log.Errorf("Failed to broadcast bond refund txn %x: %v", bond.RefundTx, err) 562 continue 563 } 564 refundCoinStr, _ = asset.DecodeCoinID(bond.AssetID, refundCoinID) 565 } else if !bondAlreadySpent { 566 c.log.Errorf("Failed to generate bond refund tx: %v", err) 567 continue 568 } 569 } else { 570 refundCoinStr, refundVal = refundCoin.String(), refundCoin.Value() 571 } 572 } 573 // RefundBond increases reserves when it spends the bond, adding to 574 // the wallet's balance (available or immature). 575 576 // If the user hasn't already manually refunded the bond, broadcast 577 // the refund txn. Mark it refunded and stop tracking regardless. 578 if bondAlreadySpent { 579 c.log.Warnf("Bond output not found, possibly already spent or never mined! "+ 580 "Marking refunded. Backup refund transaction: %x", bond.RefundTx) 581 } else { 582 subject, details := c.formatDetails(TopicBondRefunded, makeCoinIDToken(bond.CoinID.String(), bond.AssetID), acct.host, 583 makeCoinIDToken(refundCoinStr, bond.AssetID), wallet.amtString(refundVal), wallet.amtString(bond.Amount)) 584 c.notify(newBondRefundNote(TopicBondRefunded, subject, details, db.Success)) 585 } 586 587 err = c.db.BondRefunded(acct.host, assetID, bond.CoinID) 588 if err != nil { // next DB load we'll retry, hit bondAlreadySpent, and store here again 589 c.log.Errorf("Failed to mark bond as refunded: %v", err) 590 } 591 592 spentBonds = append(spentBonds, &bondID{assetID, bond.CoinID}) 593 assetIDs[assetID] = struct{}{} 594 } 595 596 // Remove spentbonds from the dexConnection's expiredBonds list. 597 acct.authMtx.Lock() 598 for _, spentBond := range spentBonds { 599 for i, bond := range acct.expiredBonds { 600 if bond.AssetID == spentBond.assetID && bytes.Equal(bond.CoinID, spentBond.coinID) { 601 acct.expiredBonds = cutBond(acct.expiredBonds, i) 602 break // next spentBond 603 } 604 } 605 } 606 expiredBondsStrength := sumBondStrengths(acct.expiredBonds, cfg.bondAssets) 607 acct.authMtx.Unlock() 608 609 return assetIDs, expiredBondsStrength, nil 610 } 611 612 // repostPendingBonds rebroadcasts all pending bond transactions for a 613 // dexConnection. 614 func (c *Core) repostPendingBonds(dc *dexConnection, cfg *dexBondCfg, state *dexAcctBondState, unlocked bool) { 615 for _, bond := range state.repost { 616 if !unlocked { // can't sign the postbond msg 617 c.log.Warnf("Cannot post pending bond for %v until account is unlocked.", dc.acct.host) 618 continue 619 } 620 // Not dependent on authed - this may be the first bond 621 // (registering) where bondConfirmed does authDEX if needed. 622 if bondAsset, ok := cfg.bondAssets[bond.AssetID]; ok { 623 c.monitorBondConfs(dc, bond, bondAsset.Confs, true) // rebroadcast 624 } else { 625 c.log.Errorf("Asset %v no longer supported by %v for bonds! "+ 626 "Pending bond to refund: %s", 627 unbip(bond.AssetID), dc.acct.host, 628 coinIDString(bond.AssetID, bond.CoinID)) 629 // Or maybe the server config will update again? Hard to know 630 // how to handle this. This really shouldn't happen though. 631 } 632 } 633 } 634 635 // postRequiredBonds posts any required bond increments for a dexConnection. 636 func (c *Core) postRequiredBonds( 637 dc *dexConnection, 638 cfg *dexBondCfg, 639 state *dexAcctBondState, 640 bondAsset *msgjson.BondAsset, 641 wallet *xcWallet, 642 expiredStrength int64, 643 unlocked bool, 644 ) (newlyBonded uint64) { 645 646 if state.TargetTier == 0 || state.mustPost <= 0 || cfg.bondExpiry <= 0 { 647 return 648 } 649 650 c.log.Infof("Gotta post %d bond increments now. Target tier %d, current bonded tier %d (%d weak, %d pending), compensating %d penalties", 651 state.mustPost, state.TargetTier, state.Rep.BondedTier, state.WeakStrength, state.PendingStrength, state.toComp) 652 653 if !unlocked || dc.status() != comms.Connected { 654 c.log.Warnf("Unable to post the required bond while disconnected or account is locked.") 655 return 656 } 657 _, err := wallet.refreshUnlock() 658 if err != nil { 659 c.log.Errorf("failed to unlock bond asset wallet %v: %v", unbip(state.BondAssetID), err) 660 return 661 } 662 err = wallet.checkPeersAndSyncStatus() 663 if err != nil { 664 c.log.Errorf("Cannot post new bonds yet. %v", err) 665 return 666 } 667 668 // For the max bonded limit, we'll normalize all bonds to the 669 // currently selected bond asset. 670 toPost := state.mustPost 671 amt := bondAsset.Amt * uint64(state.mustPost) 672 currentlyBondedAmt := uint64(state.PendingStrength+state.LiveStrength+expiredStrength) * bondAsset.Amt 673 for state.MaxBondedAmt > 0 && amt+currentlyBondedAmt > state.MaxBondedAmt && toPost > 0 { 674 toPost-- // dumber, but reads easier 675 amt = bondAsset.Amt * uint64(toPost) 676 } 677 if toPost == 0 { 678 c.log.Warnf("Unable to post new bond with equivalent of %s currently bonded (limit of %s)", 679 wallet.amtString(currentlyBondedAmt), wallet.amtString(state.MaxBondedAmt)) 680 return 681 } 682 if toPost < state.mustPost { 683 c.log.Warnf("Only posting %d bond increments instead of %d because of current bonding limit of %s", 684 toPost, state.mustPost, wallet.amtString(state.MaxBondedAmt)) 685 } 686 687 lockTime, err := c.calculateMergingLockTime(dc) 688 if err != nil { 689 c.log.Errorf("Error calculating merging locktime: %v", err) 690 return 691 } 692 693 _, err = c.makeAndPostBond(dc, true, wallet, amt, c.feeSuggestionAny(wallet.AssetID), lockTime, bondAsset) 694 if err != nil { 695 c.log.Errorf("Unable to post bond: %v", err) 696 return 697 } 698 return amt 699 } 700 701 // rotateBonds should only be run sequentially i.e. in the watchBonds loop. 702 func (c *Core) rotateBonds(ctx context.Context) { 703 // 1. Refund bonds with passed lockTime. 704 // 2. Move bonds that are expired according to DEX bond expiry into 705 // expiredBonds (lockTime<lockTimeThresh). 706 // 3. Add bonds to keep N bonds active, according to target tier and max 707 // bonded amount, posting before expiry of the bond being replaced. 708 709 if !c.bondKeysReady() { // not logged in, and nextBondKey requires login to decrypt bond xpriv 710 return // nothing to do until wallets are connected on login 711 } 712 713 now := time.Now().Unix() 714 715 for _, dc := range c.dexConnections() { 716 initialized, unlocked := dc.acct.status() 717 if !initialized { 718 continue // view-only or temporary connection 719 } 720 // Account unlocked is generally implied by bondKeysReady, but we will 721 // check per-account before post since accounts can be individually 722 // locked. However, we must refund bonds regardless. 723 724 bondCfg := c.dexBondConfig(dc, now) 725 if len(bondCfg.bondAssets) == 0 && !dc.acct.isDisabled() { 726 if !dc.IsDown() && dc.config() != nil { 727 dc.log.Meter("no-bond-assets", time.Minute*10).Warnf("Zero bond assets reported for apparently connected DCRDEX server") 728 } 729 continue 730 } 731 acctBondState := c.bondStateOfDEX(dc, bondCfg) 732 733 refundedAssets, expiredStrength, err := c.refundExpiredBonds(ctx, dc.acct, bondCfg, acctBondState, now) 734 if err != nil { 735 c.log.Errorf("Failed to refund expired bonds for %v: %v", dc.acct.host, err) 736 continue 737 } 738 for assetID := range refundedAssets { 739 c.updateAssetBalance(assetID) 740 } 741 742 if dc.acct.isDisabled() { 743 continue // For disabled account, we should only bother about unspent bonds that might have been refunded by refundExpiredBonds above. 744 } 745 746 c.repostPendingBonds(dc, bondCfg, acctBondState, unlocked) 747 748 bondAsset := bondCfg.bondAssets[acctBondState.BondAssetID] 749 if bondAsset == nil { 750 if acctBondState.TargetTier > 0 { 751 c.log.Warnf("Bond asset %d not supported by DEX %v", acctBondState.BondAssetID, dc.acct.host) 752 } 753 continue 754 } 755 756 wallet, err := c.connectedWallet(acctBondState.BondAssetID) 757 if err != nil { 758 if acctBondState.TargetTier > 0 { 759 c.log.Errorf("%v wallet not available for bonds: %v", unbip(acctBondState.BondAssetID), err) 760 } 761 continue 762 } 763 764 c.postRequiredBonds(dc, bondCfg, acctBondState, bondAsset, wallet, expiredStrength, unlocked) 765 } 766 767 c.updateBondReserves() 768 } 769 770 func (c *Core) preValidateBond(dc *dexConnection, bond *asset.Bond) error { 771 if len(dc.acct.encKey) == 0 { 772 return fmt.Errorf("uninitialized account") 773 } 774 775 pkBytes := dc.acct.pubKey() 776 if len(pkBytes) == 0 { 777 return fmt.Errorf("account keys not decrypted") 778 } 779 780 // Pre-validate with the raw bytes of the unsigned tx and our account 781 // pubkey. 782 preBond := &msgjson.PreValidateBond{ 783 AcctPubKey: pkBytes, 784 AssetID: bond.AssetID, 785 Version: bond.Version, 786 RawTx: bond.UnsignedTx, 787 } 788 789 preBondRes := new(msgjson.PreValidateBondResult) 790 err := dc.signAndRequest(preBond, msgjson.PreValidateBondRoute, preBondRes, DefaultResponseTimeout) 791 if err != nil { 792 return codedError(registerErr, err) 793 } 794 // Check the response signature. 795 err = dc.acct.checkSig(append(preBondRes.Serialize(), bond.UnsignedTx...), preBondRes.Sig) 796 if err != nil { 797 return newError(signatureErr, "preValidateBond: DEX signature validation error: %v", err) 798 } 799 800 if preBondRes.Amount != bond.Amount { 801 return newError(bondTimeErr, "pre-validated bond amount is not the desired amount: %d != %d", 802 preBondRes.Amount, bond.Amount) 803 } 804 805 return nil 806 } 807 808 func (c *Core) postBond(dc *dexConnection, bond *asset.Bond) (*msgjson.PostBondResult, error) { 809 if len(dc.acct.encKey) == 0 { 810 return nil, fmt.Errorf("uninitialized account") 811 } 812 813 pkBytes := dc.acct.pubKey() 814 if len(pkBytes) == 0 { 815 return nil, fmt.Errorf("account keys not decrypted") 816 } 817 assetID, bondCoin := bond.AssetID, bond.CoinID 818 bondCoinStr := coinIDString(assetID, bondCoin) 819 820 // Do a postbond request with the raw bytes of the unsigned tx, the bond 821 // script, and our account pubkey. 822 postBond := &msgjson.PostBond{ 823 AcctPubKey: pkBytes, 824 AssetID: assetID, 825 Version: bond.Version, 826 CoinID: bondCoin, 827 } 828 829 postBondRes := new(msgjson.PostBondResult) 830 err := dc.signAndRequest(postBond, msgjson.PostBondRoute, postBondRes, DefaultResponseTimeout) 831 if err != nil { 832 return nil, codedError(registerErr, err) 833 } 834 835 // Check the response signature. 836 err = dc.acct.checkSig(postBondRes.Serialize(), postBondRes.Sig) 837 if err != nil { 838 c.log.Warnf("postbond: DEX signature validation error: %v", err) 839 } 840 841 if !bytes.Equal(postBondRes.BondID, bondCoin) { 842 return nil, fmt.Errorf("server reported bond coin ID %v, expected %v", coinIDString(assetID, postBondRes.BondID), 843 bondCoinStr) 844 } 845 846 dc.acct.authMtx.Lock() 847 dc.updateReputation(postBondRes.Reputation) 848 dc.acct.authMtx.Unlock() 849 850 return postBondRes, nil 851 } 852 853 // postAndConfirmBond submits a postbond request for the given bond. 854 func (c *Core) postAndConfirmBond(dc *dexConnection, bond *asset.Bond) { 855 assetID, coinID := bond.AssetID, bond.CoinID 856 coinIDStr := coinIDString(assetID, coinID) 857 858 // Inform the server, which will attempt to locate the bond and check 859 // confirmations. If server sees the required number of confirmations, the 860 // bond will be active (and account created if new) and we should confirm 861 // the bond (in DB and dc.acct.{bond,pendingBonds}). 862 pbr, err := c.postBond(dc, bond) // can be long while server searches 863 if err != nil { 864 subject, details := c.formatDetails(TopicBondPostError, err, err) 865 c.notify(newBondPostNote(TopicBondPostError, subject, details, db.ErrorLevel, dc.acct.host)) 866 return 867 } 868 869 c.log.Infof("Bond confirmed %v (%s) with expire time of %v", coinIDStr, 870 unbip(assetID), time.Unix(int64(pbr.Expiry), 0)) 871 err = c.bondConfirmed(dc, assetID, coinID, pbr) 872 if err != nil { 873 c.log.Errorf("Unable to confirm bond: %v", err) 874 } 875 } 876 877 // monitorBondConfs launches a block waiter for the bond txns to reach the 878 // required amount of confirmations. Once the requirement is met the server is 879 // notified. 880 func (c *Core) monitorBondConfs(dc *dexConnection, bond *asset.Bond, reqConfs uint32, rebroadcast ...bool) { 881 assetID, coinID := bond.AssetID, bond.CoinID 882 coinIDStr := coinIDString(assetID, coinID) 883 host := dc.acct.host 884 885 wallet, err := c.connectedWallet(assetID) 886 if err != nil { 887 c.log.Errorf("No connected wallet for asset %v: %v", unbip(assetID), err) 888 return 889 } 890 lastConfs, err := wallet.RegFeeConfirmations(c.ctx, coinID) 891 coinNotFound := errors.Is(err, asset.CoinNotFoundError) 892 if err != nil && !coinNotFound { 893 c.log.Errorf("Error getting confirmations for %s: %w", coinIDStr, err) 894 return 895 } 896 897 if lastConfs >= reqConfs { // don't bother waiting for a block 898 go c.postAndConfirmBond(dc, bond) 899 return 900 } 901 902 if coinNotFound || (len(rebroadcast) > 0 && rebroadcast[0]) { 903 // Broadcast the bond and start waiting for confs. 904 c.log.Infof("Rebroadcasting bond %v (%s), data = %x.\n\n"+ 905 "BACKUP refund tx paying to current wallet: %x\n\n", 906 coinIDStr, unbip(bond.AssetID), bond.Data, bond.RedeemTx) 907 c.log.Tracef("Raw bond transaction: %x", bond.SignedTx) 908 if _, err = wallet.SendTransaction(bond.SignedTx); err != nil { 909 c.log.Warnf("Failed to broadcast bond txn (%v): Tx bytes %x", err, bond.SignedTx) 910 // TODO: screen inputs if the tx is trying to spend spent outputs 911 // (invalid bond transaction that should be abandoned). 912 } 913 c.updateAssetBalance(bond.AssetID) 914 } 915 916 c.updatePendingBondConfs(dc, bond.AssetID, bond.CoinID, lastConfs) 917 918 trigger := func() (bool, error) { 919 // Retrieve the current wallet in case it was reconfigured. 920 wallet, _ := c.wallet(assetID) // We already know the wallet is there by now. 921 confs, err := wallet.RegFeeConfirmations(c.ctx, coinID) 922 if err != nil && !errors.Is(err, asset.CoinNotFoundError) { 923 return false, fmt.Errorf("Error getting confirmations for %s: %w", coinIDStr, err) 924 } 925 926 if confs != lastConfs { 927 c.updateAssetBalance(assetID) 928 lastConfs = confs 929 c.updatePendingBondConfs(dc, bond.AssetID, bond.CoinID, confs) 930 } 931 932 if confs < reqConfs { 933 details := fmt.Sprintf("Bond confirmations %v/%v", confs, reqConfs) 934 c.notify(newBondPostNoteWithConfirmations(TopicRegUpdate, string(TopicRegUpdate), 935 details, db.Data, assetID, coinIDStr, int32(confs), host, c.exchangeAuth(dc))) 936 } 937 938 return confs >= reqConfs, nil 939 } 940 941 c.wait(coinID, assetID, trigger, func(err error) { 942 if err != nil { 943 subject, details := c.formatDetails(TopicBondPostErrorConfirm, host, err) 944 c.notify(newBondPostNote(TopicBondPostError, subject, details, db.ErrorLevel, host)) 945 return 946 } 947 948 c.log.Infof("DEX %v bond txn %s now has %d confirmations. Submitting postbond request...", 949 host, coinIDStr, reqConfs) 950 951 c.postAndConfirmBond(dc, bond) // if it fails (e.g. timeout), retry in rotateBonds 952 }) 953 } 954 955 // RedeemPrepaidBond redeems a pre-paid bond for a dcrdex host server. 956 func (c *Core) RedeemPrepaidBond(appPW []byte, code []byte, host string, certI any) (tier uint64, err error) { 957 // Make sure the app has been initialized. 958 if !c.IsInitialized() { 959 return 0, fmt.Errorf("app not initialized") 960 } 961 962 // Check the app password. 963 crypter, err := c.encryptionKey(appPW) 964 if err != nil { 965 return 0, codedError(passwordErr, err) 966 } 967 defer crypter.Close() 968 969 var success, acctExists bool 970 971 c.connMtx.RLock() 972 dc, found := c.conns[host] 973 c.connMtx.RUnlock() 974 if found { 975 acctExists = !dc.acct.isViewOnly() 976 if acctExists { 977 if dc.acct.locked() { // require authDEX first to reconcile any existing bond statuses 978 return 0, newError(acctKeyErr, "acct locked %s (login first)", host) 979 } 980 } 981 } else { 982 // New DEX connection. 983 cert, err := parseCert(host, certI, c.net) 984 if err != nil { 985 return 0, newError(fileReadErr, "failed to read certificate file from %s: %v", cert, err) 986 } 987 dc, err = c.connectDEX(&db.AccountInfo{ 988 Host: host, 989 Cert: cert, 990 // bond maintenance options set below. 991 }) 992 if err != nil { 993 return 0, codedError(connectionErr, err) 994 } 995 996 // Close the connection to the dex server if the registration fails. 997 defer func() { 998 if !success { 999 dc.connMaster.Disconnect() 1000 } 1001 }() 1002 } 1003 1004 if !acctExists { // new dex connection or pre-existing view-only connection 1005 _, err := c.discoverAccount(dc, crypter) 1006 if err != nil { 1007 return 0, err 1008 } 1009 } 1010 1011 pkBytes := dc.acct.pubKey() 1012 if len(pkBytes) == 0 { 1013 return 0, fmt.Errorf("account keys not decrypted") 1014 } 1015 1016 // Do a postbond request with the raw bytes of the unsigned tx, the bond 1017 // script, and our account pubkey. 1018 postBond := &msgjson.PostBond{ 1019 AcctPubKey: pkBytes, 1020 AssetID: account.PrepaidBondID, 1021 // Version: 0, 1022 CoinID: code, 1023 } 1024 postBondRes := new(msgjson.PostBondResult) 1025 if err = dc.signAndRequest(postBond, msgjson.PostBondRoute, postBondRes, DefaultResponseTimeout); err != nil { 1026 return 0, codedError(registerErr, err) 1027 } 1028 1029 // Check the response signature. 1030 err = dc.acct.checkSig(postBondRes.Serialize(), postBondRes.Sig) 1031 if err != nil { 1032 c.log.Warnf("postbond: DEX signature validation error: %v", err) 1033 } 1034 1035 lockTime := postBondRes.Expiry + dc.config().BondExpiry 1036 1037 dbBond := &db.Bond{ 1038 // Version: 0, 1039 AssetID: account.PrepaidBondID, 1040 CoinID: code, 1041 LockTime: lockTime, 1042 Strength: postBondRes.Strength, 1043 Confirmed: true, 1044 } 1045 1046 dc.acct.authMtx.Lock() 1047 dc.updateReputation(postBondRes.Reputation) 1048 dc.acct.bonds = append(dc.acct.bonds, dbBond) 1049 dc.acct.authMtx.Unlock() 1050 1051 if !acctExists { 1052 dc.acct.keyMtx.RLock() 1053 ai := &db.AccountInfo{ 1054 Host: dc.acct.host, 1055 Cert: dc.acct.cert, 1056 DEXPubKey: dc.acct.dexPubKey, 1057 EncKeyV2: dc.acct.encKey, 1058 Bonds: []*db.Bond{dbBond}, 1059 } 1060 dc.acct.keyMtx.RUnlock() 1061 1062 if err = c.dbCreateOrUpdateAccount(dc, ai); err != nil { 1063 return 0, fmt.Errorf("failed to store pre-paid account for dex %s: %w", host, err) 1064 } 1065 c.addDexConnection(dc) 1066 } 1067 1068 success = true // Don't disconnect anymore. 1069 1070 if err = c.db.AddBond(dc.acct.host, dbBond); err != nil { 1071 return 0, fmt.Errorf("failed to store pre-paid bond for dex %s: %w", host, err) 1072 } 1073 1074 if err = c.bondConfirmed(dc, account.PrepaidBondID, code, postBondRes); err != nil { 1075 return 0, fmt.Errorf("bond redeemed, but failed to auth: %v", err) 1076 } 1077 1078 c.updateBondReserves() 1079 1080 c.notify(newBondAuthUpdate(dc.acct.host, c.exchangeAuth(dc))) 1081 1082 return uint64(postBondRes.Strength), nil 1083 } 1084 1085 func deriveBondKey(bondXPriv *hdkeychain.ExtendedKey, assetID, bondIndex uint32) (*secp256k1.PrivateKey, error) { 1086 kids := []uint32{ 1087 assetID + hdkeychain.HardenedKeyStart, 1088 bondIndex, 1089 } 1090 extKey, err := keygen.GenDeepChildFromXPriv(bondXPriv, kids) 1091 if err != nil { 1092 return nil, fmt.Errorf("GenDeepChild error: %w", err) 1093 } 1094 privB, err := extKey.SerializedPrivKey() 1095 if err != nil { 1096 return nil, fmt.Errorf("SerializedPrivKey error: %w", err) 1097 } 1098 priv := secp256k1.PrivKeyFromBytes(privB) 1099 return priv, nil 1100 } 1101 1102 func deriveBondXPriv(seed []byte) (*hdkeychain.ExtendedKey, error) { 1103 return keygen.GenDeepChild(seed, []uint32{hdKeyPurposeBonds}) 1104 } 1105 1106 func (c *Core) bondKeyIdx(assetID, idx uint32) (*secp256k1.PrivateKey, error) { 1107 c.loginMtx.Lock() 1108 defer c.loginMtx.Unlock() 1109 1110 if c.bondXPriv == nil { 1111 return nil, errors.New("not logged in") 1112 } 1113 1114 return deriveBondKey(c.bondXPriv, assetID, idx) 1115 } 1116 1117 // nextBondKey generates the private key for the next bond, incrementing a 1118 // persistent bond index counter. This method requires login to decrypt and set 1119 // the bond xpriv, so use the bondKeysReady method to ensure it is ready first. 1120 // The bond key index is returned so the same key may be regenerated. 1121 func (c *Core) nextBondKey(assetID uint32) (*secp256k1.PrivateKey, uint32, error) { 1122 nextBondKeyIndex, err := c.db.NextBondKeyIndex(assetID) 1123 if err != nil { 1124 return nil, 0, fmt.Errorf("NextBondIndex: %v", err) 1125 } 1126 1127 priv, err := c.bondKeyIdx(assetID, nextBondKeyIndex) 1128 if err != nil { 1129 return nil, 0, fmt.Errorf("bondKeyIdx: %v", err) 1130 } 1131 return priv, nextBondKeyIndex, nil 1132 } 1133 1134 // UpdateBondOptions sets the bond rotation options for a DEX host, including 1135 // the target trading tier, the preferred asset to use for bonds, and the 1136 // maximum amount allowable to be locked in bonds. 1137 func (c *Core) UpdateBondOptions(form *BondOptionsForm) error { 1138 dc, _, err := c.dex(form.Host) 1139 if err != nil { 1140 return err 1141 } 1142 // TODO: exclude unregistered and/or watch-only 1143 dbAcct, err := c.db.Account(form.Host) 1144 if err != nil { 1145 return err 1146 } 1147 1148 bondAssets, _ := dc.bondAssets() 1149 if bondAssets == nil { 1150 c.log.Warnf("DEX host %v is offline. Bond reconfiguration options are limited to disabling.", 1151 dc.acct.host) 1152 } 1153 1154 // For certain changes, update one or more wallet balances when done. 1155 var tierChanged, assetChanged bool 1156 var wallet *xcWallet // new wallet 1157 var bondAssetID0 uint32 // old wallet's asset ID 1158 var targetTier0, maxBondedAmt0 uint64 1159 var penaltyComps0 uint16 1160 defer func() { 1161 if (tierChanged || assetChanged) && (wallet != nil) { 1162 if _, err := c.updateWalletBalance(wallet); err != nil { 1163 c.log.Errorf("Unable to set balance for wallet %v", wallet.Symbol) 1164 } 1165 if wallet.AssetID != bondAssetID0 && targetTier0 > 0 { 1166 c.updateAssetBalance(bondAssetID0) 1167 } 1168 } 1169 }() 1170 1171 var success bool 1172 dc.acct.authMtx.Lock() 1173 defer func() { 1174 dc.acct.authMtx.Unlock() 1175 if success { 1176 c.notify(newBondAuthUpdate(dc.acct.host, c.exchangeAuth(dc))) 1177 } 1178 }() 1179 1180 if !dc.acct.isAuthed { 1181 return errors.New("login or register first") 1182 } 1183 1184 // Revert to initial values if we encounter any error below. 1185 bondAssetID0 = dc.acct.bondAsset 1186 targetTier0, maxBondedAmt0, penaltyComps0 = dc.acct.targetTier, dc.acct.maxBondedAmt, dc.acct.penaltyComps 1187 defer func() { // still under authMtx lock on defer stack 1188 if !success { 1189 dc.acct.bondAsset = bondAssetID0 1190 dc.acct.maxBondedAmt = maxBondedAmt0 1191 dc.acct.penaltyComps = penaltyComps0 1192 if dc.acct.targetTier > 0 || assetChanged { 1193 dc.acct.targetTier = targetTier0 1194 } // else the user was trying to clear target tier and the wallet was gone too 1195 } 1196 }() 1197 1198 // Verify the new bond asset wallet first. 1199 bondAssetID := bondAssetID0 1200 if form.BondAssetID != nil { 1201 bondAssetID = *form.BondAssetID 1202 } 1203 assetChanged = bondAssetID != bondAssetID0 1204 1205 targetTier := targetTier0 1206 if form.TargetTier != nil { 1207 targetTier = *form.TargetTier 1208 } 1209 tierChanged = targetTier != targetTier0 1210 if tierChanged { 1211 dc.acct.targetTier = targetTier 1212 dbAcct.TargetTier = targetTier 1213 } 1214 1215 var penaltyComps = penaltyComps0 1216 if form.PenaltyComps != nil { 1217 penaltyComps = *form.PenaltyComps 1218 } 1219 dc.acct.penaltyComps = penaltyComps 1220 dbAcct.PenaltyComps = penaltyComps 1221 1222 var bondAssetAmt uint64 // because to disable we must proceed even with no config 1223 bondAsset := bondAssets[bondAssetID] 1224 if bondAsset == nil { 1225 if targetTier > 0 || assetChanged { 1226 return fmt.Errorf("dex %v is does not support %v as a bond asset (or we lack their config)", 1227 dbAcct.Host, unbip(bondAssetID)) 1228 } // else disable, attempting to unreserve funds if wallet is available 1229 } else { 1230 bondAssetAmt = bondAsset.Amt 1231 } 1232 1233 // If we're lowering our bond, we can't set the max bonded amount too low. 1234 tierForDefaultMaxBonded := targetTier 1235 if targetTier > 0 && targetTier0 > targetTier { 1236 tierForDefaultMaxBonded = targetTier0 1237 } 1238 1239 maxBonded := maxBondedMult * bondAssetAmt * (tierForDefaultMaxBonded + uint64(penaltyComps)) // the min if none specified 1240 if form.MaxBondedAmt != nil { 1241 requested := *form.MaxBondedAmt 1242 if requested < maxBonded { 1243 return fmt.Errorf("requested bond maximum of %d is lower than minimum of %d", requested, maxBonded) 1244 } 1245 maxBonded = requested 1246 } 1247 1248 var found bool 1249 wallet, found = c.wallet(bondAssetID) 1250 if !found || !wallet.connected() { 1251 return fmt.Errorf("bond asset wallet %v does not exist or is not connected", unbip(bondAssetID)) 1252 } 1253 bonder, ok := wallet.Wallet.(asset.Bonder) 1254 if !ok { 1255 return fmt.Errorf("wallet %v is not an asset.Bonder", unbip(bondAssetID)) 1256 } 1257 1258 _, err = wallet.refreshUnlock() 1259 if err != nil { 1260 return fmt.Errorf("bond asset wallet %v is locked", unbip(bondAssetID)) 1261 } 1262 1263 if assetChanged || tierChanged { 1264 bal, err := wallet.Balance() 1265 if err != nil { 1266 return fmt.Errorf("failed to get balance for %s wallet: %w", unbip(bondAssetID), err) 1267 } 1268 avail := bal.Available + bal.BondReserves 1269 1270 // We need to recalculate bond reserves, including all other exchanges. 1271 // We're under the dc.acct.authMtx lock, so we'll add our contribution 1272 // first and then iterate the others in a loop where we're okay to lock 1273 // their authMtx (via bondTotal). 1274 nominalReserves := c.minBondReserves(dc, bondAsset) 1275 var n uint64 1276 if targetTier > 0 { 1277 n = 1 1278 } 1279 var tiers uint64 = targetTier 1280 for _, otherDC := range c.dexConnections() { 1281 if otherDC.acct.host == dc.acct.host { // Only adding others 1282 continue 1283 } 1284 assetID, _, _ := otherDC.bondOpts() 1285 if assetID != bondAssetID { 1286 continue 1287 } 1288 bondAsset, _ := otherDC.bondAsset(assetID) 1289 if bondAsset == nil { 1290 continue 1291 } 1292 n++ 1293 tiers += targetTier 1294 ba := BondAsset(*bondAsset) 1295 otherDC.acct.authMtx.RLock() 1296 nominalReserves += c.minBondReserves(dc, &ba) 1297 otherDC.acct.authMtx.RUnlock() 1298 } 1299 1300 var feeReserves uint64 1301 if n > 0 { 1302 feeBuffer := bonder.BondsFeeBuffer(c.feeSuggestionAny(bondAssetID)) 1303 feeReserves = n * feeBuffer 1304 req := nominalReserves + feeReserves 1305 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", 1306 n, unbip(bondAssetID), tiers, req, feeReserves, avail) 1307 // If raising the tier or changing asset, enforce available funds. 1308 if (assetChanged || targetTier > targetTier0) && req > avail { 1309 return fmt.Errorf("insufficient funds. need %d, have %d", req, avail) 1310 } 1311 } 1312 1313 bonder.SetBondReserves(nominalReserves + feeReserves) 1314 1315 dc.acct.bondAsset = bondAssetID 1316 dbAcct.BondAsset = bondAssetID 1317 } 1318 1319 if assetChanged || tierChanged || form.MaxBondedAmt != nil || maxBonded < dc.acct.maxBondedAmt { 1320 dc.acct.maxBondedAmt = maxBonded 1321 dbAcct.MaxBondedAmt = maxBonded 1322 } 1323 1324 c.triggerBondRotation() 1325 1326 c.log.Debugf("Bond options for %v: target tier %d, bond asset %d, maxBonded %v", 1327 dbAcct.Host, dc.acct.targetTier, dc.acct.bondAsset, dbAcct.MaxBondedAmt) 1328 1329 if err = c.db.UpdateAccountInfo(dbAcct); err == nil { 1330 success = true 1331 } // else we might have already done ReserveBondFunds... 1332 return err 1333 1334 } 1335 1336 // BondsFeeBuffer suggests how much extra may be required for the transaction 1337 // fees part of bond reserves when bond rotation is enabled. This may be used to 1338 // inform the consumer how much extra (beyond double the bond amount) is 1339 // required to facilitate uninterrupted maintenance of a target trading tier. 1340 func (c *Core) BondsFeeBuffer(assetID uint32) (uint64, error) { 1341 wallet, err := c.connectedWallet(assetID) 1342 if err != nil { 1343 return 0, err 1344 } 1345 bonder, ok := wallet.Wallet.(asset.Bonder) 1346 if !ok { 1347 return 0, errors.New("wallet does not support bonds") 1348 } 1349 return bonder.BondsFeeBuffer(c.feeSuggestionAny(assetID)), nil 1350 } 1351 1352 // PostBond begins the process of posting a new bond for a new or existing DEX 1353 // account. On return, the bond transaction will have been broadcast, and when 1354 // the required number of confirmations is reached, Core will submit the bond 1355 // for acceptance to the server. A TopicBondConfirmed is emitted when the 1356 // fully-confirmed bond is accepted. Before the transaction is broadcasted, a 1357 // prevalidatebond request is sent to ensure the transaction is compliant and 1358 // (and that the intended server is actually online!). PostBond may be used to 1359 // create a new account with a bond, or to top-up bond on an existing account. 1360 // If the account is not yet configured in Core, account discovery will be 1361 // performed prior to posting a new bond. If account discovery finds an existing 1362 // account, the connection is established but no additional bond is posted. If 1363 // no account is discovered on the server, the account is created locally and 1364 // bond is posted to create the account. 1365 // 1366 // Note that the FeeBuffer field of the form is optional, but it may be provided 1367 // to ensure that the wallet reserves the amount reported by a preceding call to 1368 // BondsFeeBuffer, such as during initial wallet funding. 1369 func (c *Core) PostBond(form *PostBondForm) (*PostBondResult, error) { 1370 // Make sure the app has been initialized. 1371 if !c.IsInitialized() { 1372 return nil, fmt.Errorf("app not initialized") 1373 } 1374 1375 // Check that the bond amount is non-zero before we touch wallets and make 1376 // connections to the DEX host. 1377 if form.Bond == 0 { 1378 return nil, newError(bondAmtErr, "zero registration fees not allowed") 1379 } 1380 1381 // Get the wallet to author the transaction. Default to DCR. 1382 bondAssetID := uint32(42) 1383 if form.Asset != nil { 1384 bondAssetID = *form.Asset 1385 } 1386 bondAssetSymbol := dex.BipIDSymbol(bondAssetID) 1387 wallet, err := c.connectedWallet(bondAssetID) 1388 if err != nil { 1389 return nil, fmt.Errorf("cannot connect to %s wallet to pay fee: %w", bondAssetSymbol, err) 1390 } 1391 if _, ok := wallet.Wallet.(asset.Bonder); !ok { // will fail in MakeBondTx, but assert early 1392 return nil, fmt.Errorf("wallet %v is not an asset.Bonder", bondAssetSymbol) 1393 } 1394 err = wallet.checkPeersAndSyncStatus() 1395 if err != nil { 1396 return nil, err 1397 } 1398 1399 // Check the app password. 1400 crypter, err := c.encryptionKey(form.AppPass) 1401 if err != nil { 1402 return nil, codedError(passwordErr, err) 1403 } 1404 defer crypter.Close() 1405 if form.Addr == "" { 1406 return nil, newError(emptyHostErr, "no dex address specified") 1407 } 1408 host, err := addrHost(form.Addr) 1409 if err != nil { 1410 return nil, newError(addressParseErr, "error parsing address: %v", err) 1411 } 1412 1413 // Get ready to generate the bond txn. 1414 if !wallet.unlocked() { 1415 err = wallet.Unlock(crypter) 1416 if err != nil { 1417 return nil, newError(walletAuthErr, "failed to unlock %s wallet: %v", unbip(wallet.AssetID), err) 1418 } 1419 } 1420 1421 var success, acctExists bool 1422 1423 // When creating an account or registering a view-only account, the default 1424 // is to maintain tier. 1425 maintain := true 1426 if form.MaintainTier != nil { 1427 maintain = *form.MaintainTier 1428 } 1429 1430 c.connMtx.RLock() 1431 dc, found := c.conns[host] 1432 c.connMtx.RUnlock() 1433 if found { 1434 acctExists = !dc.acct.isViewOnly() 1435 if acctExists { 1436 if dc.acct.locked() { // require authDEX first to reconcile any existing bond statuses 1437 return nil, newError(acctKeyErr, "acct locked %s (login first)", form.Addr) 1438 } 1439 if form.MaintainTier != nil || form.MaxBondedAmt != nil { 1440 return nil, fmt.Errorf("maintain tier and max bonded amount may only be set when registering " + 1441 "(use UpdateBondOptions to change bond maintenance settings)") 1442 } 1443 } 1444 } else { 1445 // Before connecting to the DEX host, do a quick balance check to ensure 1446 // we at least have the nominal bond amount available. 1447 if bal, err := wallet.Balance(); err != nil { 1448 return nil, newError(bondAssetErr, "unable to check wallet balance: %w", err) 1449 } else if bal.Available < form.Bond { 1450 return nil, newError(walletBalanceErr, "insufficient available balance") 1451 } 1452 1453 // New DEX connection. 1454 cert, err := parseCert(host, form.Cert, c.net) 1455 if err != nil { 1456 return nil, newError(fileReadErr, "failed to read certificate file from %s: %v", cert, err) 1457 } 1458 dc, err = c.connectDEX(&db.AccountInfo{ 1459 Host: host, 1460 Cert: cert, 1461 // bond maintenance options set below. 1462 }) 1463 if err != nil { 1464 return nil, codedError(connectionErr, err) 1465 } 1466 1467 // Close the connection to the dex server if the registration fails. 1468 defer func() { 1469 if !success { 1470 dc.connMaster.Disconnect() 1471 } 1472 }() 1473 } 1474 1475 if !acctExists { // new dex connection or pre-existing view-only connection 1476 paid, err := c.discoverAccount(dc, crypter) 1477 if err != nil { 1478 return nil, err 1479 } 1480 // dc.acct is now configured with encKey, privKey, and id for a new 1481 // (unregistered) account. 1482 if paid { 1483 success = true 1484 // The listen goroutine is already running, now track the conn. 1485 c.addDexConnection(dc) 1486 return &PostBondResult{ /* no new bond */ }, nil 1487 } 1488 } 1489 1490 feeRate := c.feeSuggestionAny(bondAssetID, dc) 1491 1492 // Ensure this DEX supports this asset for bond, and get the required 1493 // confirmations and bond amount. 1494 bondAsset, _ := dc.bondAsset(bondAssetID) 1495 if bondAsset == nil { 1496 return nil, newError(assetSupportErr, "dex host has not connected or does not support fidelity bonds in asset %q", bondAssetSymbol) 1497 } 1498 1499 var lockTime time.Time 1500 if form.LockTime > 0 { 1501 lockTime = time.Unix(int64(form.LockTime), 0) 1502 } else { 1503 lockTime, err = c.calculateMergingLockTime(dc) 1504 if err != nil { 1505 return nil, err 1506 } 1507 } 1508 1509 // Check that the bond amount matches the caller's expectations. 1510 if form.Bond < bondAsset.Amt { 1511 return nil, newError(bondAmtErr, "specified bond amount is less than the DEX-provided amount. %d < %d", 1512 form.Bond, bondAsset.Amt) 1513 } 1514 if rem := form.Bond % bondAsset.Amt; rem != 0 { 1515 return nil, newError(bondAmtErr, "specified bond amount is not a multiple of the DEX-provided amount. %d %% %d = %d", 1516 form.Bond, bondAsset.Amt, rem) 1517 } 1518 if acctExists { // if account exists, advise using UpdateBondOptions 1519 autoBondAsset, targetTier, maxBondedAmt := dc.bondOpts() 1520 c.log.Warnf("Manually posting bond for existing account "+ 1521 "(target tier %d, bond asset %d, maxBonded %v). "+ 1522 "Consider using UpdateBondOptions instead.", 1523 targetTier, autoBondAsset, wallet.amtString(maxBondedAmt)) 1524 } else if maintain { // new account (or registering a view-only acct) with tier maintenance enabled 1525 // Fully pre-reserve funding with the wallet before making and 1526 // transactions. bondConfirmed will call authDEX, which will recognize 1527 // that it is the first authorization of the account with the DEX via 1528 // the totalReserves and isAuthed fields of dexAccount. 1529 maxBondedAmt := maxBondedMult * form.Bond // default 1530 if form.MaxBondedAmt != nil { 1531 maxBondedAmt = *form.MaxBondedAmt 1532 } 1533 dc.acct.authMtx.Lock() 1534 dc.acct.bondAsset = bondAssetID 1535 dc.acct.targetTier = form.Bond / bondAsset.Amt 1536 dc.acct.maxBondedAmt = maxBondedAmt 1537 dc.acct.authMtx.Unlock() 1538 } 1539 1540 // Make a bond transaction for the account ID generated from our public key. 1541 bondCoin, err := c.makeAndPostBond(dc, acctExists, wallet, form.Bond, feeRate, lockTime, bondAsset) 1542 if err != nil { 1543 return nil, err 1544 } 1545 c.updateBondReserves() // Can probably reduce reserves because of the pending bond. 1546 success = true 1547 bondCoinStr := coinIDString(bondAssetID, bondCoin) 1548 return &PostBondResult{BondID: bondCoinStr, ReqConfirms: uint16(bondAsset.Confs)}, nil 1549 } 1550 1551 // calculateMergingLockTime calculates a locktime for a new bond for the 1552 // specified account, with consideration for merging parallel bond tracks. 1553 // Tracks are merged by choosing the locktime of an existing bond if one exists 1554 // and has a locktime value in an acceptable range. We will merge tracks even if 1555 // it means reducing the live period associated with the bond by as much as 1556 // ~75%. 1557 func (c *Core) calculateMergingLockTime(dc *dexConnection) (time.Time, error) { 1558 bondExpiry := int64(dc.config().BondExpiry) 1559 lockDur := minBondLifetime(c.net, bondExpiry) 1560 lockTime := time.Now().Add(lockDur).Truncate(time.Second) 1561 expireTime := lockTime.Add(time.Second * time.Duration(-bondExpiry)) // when the server would expire the bond 1562 if time.Until(expireTime) < time.Minute { 1563 return time.Time{}, newError(bondTimeErr, "bond would expire in less than one minute") 1564 } 1565 if lockDur := time.Until(lockTime); lockDur > lockTimeLimit { 1566 return time.Time{}, newError(bondTimeErr, "excessive lock time (%v>%v)", lockDur, lockTimeLimit) 1567 } 1568 1569 // If we have parallel bond tracks out of sync, we may use an earlier lock 1570 // time in order to get back in sync. 1571 mergeableLocktimeThresh := uint64(time.Now().Unix() + bondExpiry*5/4 + pendingBuffer(c.net)) 1572 var bestMergeableLocktime uint64 1573 dc.acct.authMtx.RLock() 1574 for _, b := range dc.acct.bonds { 1575 if b.LockTime > mergeableLocktimeThresh && (bestMergeableLocktime == 0 || b.LockTime > bestMergeableLocktime) { 1576 bestMergeableLocktime = b.LockTime 1577 } 1578 } 1579 dc.acct.authMtx.RUnlock() 1580 if bestMergeableLocktime > 0 { 1581 newLockTime := time.Unix(int64(bestMergeableLocktime), 0) 1582 bondExpiryDur := time.Duration(bondExpiry) * time.Second 1583 c.log.Infof("Reducing bond expiration date from %s to %s to facilitate merge with parallel bond track", 1584 lockTime.Add(-bondExpiryDur), newLockTime.Add(-bondExpiryDur)) 1585 lockTime = newLockTime 1586 } 1587 return lockTime, nil 1588 } 1589 1590 func (c *Core) makeAndPostBond(dc *dexConnection, acctExists bool, wallet *xcWallet, amt, feeRate uint64, 1591 lockTime time.Time, bondAsset *msgjson.BondAsset) ([]byte, error) { 1592 1593 bondKey, keyIndex, err := c.nextBondKey(bondAsset.ID) 1594 if err != nil { 1595 return nil, fmt.Errorf("bond key derivation failed: %v", err) 1596 } 1597 defer bondKey.Zero() 1598 1599 acctID := dc.acct.ID() 1600 bond, abandon, err := wallet.MakeBondTx(bondAsset.Version, amt, feeRate, lockTime, bondKey, acctID[:]) 1601 if err != nil { 1602 return nil, codedError(bondPostErr, err) 1603 } 1604 // MakeBondTx lock coins and reduces reserves in proportion 1605 1606 var success bool 1607 defer func() { 1608 if !success { 1609 abandon() // unlock coins and increase reserves 1610 } 1611 }() 1612 1613 // Do prevalidatebond with the *unsigned* txn. 1614 if err = c.preValidateBond(dc, bond); err != nil { 1615 return nil, err 1616 } 1617 1618 reqConfs := bondAsset.Confs 1619 bondCoinStr := coinIDString(bond.AssetID, bond.CoinID) 1620 c.log.Infof("DEX %v has validated our bond %v (%s) with strength %d. %d confirmations required to trade.", 1621 dc.acct.host, bondCoinStr, unbip(bond.AssetID), amt/bondAsset.Amt, reqConfs) 1622 1623 // Store the account and bond info. 1624 dbBond := &db.Bond{ 1625 Version: bond.Version, 1626 AssetID: bond.AssetID, 1627 CoinID: bond.CoinID, 1628 UnsignedTx: bond.UnsignedTx, 1629 SignedTx: bond.SignedTx, 1630 Data: bond.Data, 1631 Amount: amt, 1632 LockTime: uint64(lockTime.Unix()), 1633 KeyIndex: keyIndex, 1634 RefundTx: bond.RedeemTx, 1635 Strength: uint32(amt / bondAsset.Amt), 1636 // Confirmed and Refunded are false (new bond tx) 1637 } 1638 1639 if acctExists { 1640 err = c.db.AddBond(dc.acct.host, dbBond) 1641 if err != nil { 1642 return nil, fmt.Errorf("failed to store bond %v (%s) for dex %v: %w", 1643 bondCoinStr, unbip(bond.AssetID), dc.acct.host, err) 1644 } 1645 } else { 1646 bondAsset, targetTier, maxBondedAmt := dc.bondOpts() 1647 ai := &db.AccountInfo{ 1648 Host: dc.acct.host, 1649 Cert: dc.acct.cert, 1650 DEXPubKey: dc.acct.dexPubKey, 1651 EncKeyV2: dc.acct.encKey, 1652 Bonds: []*db.Bond{dbBond}, 1653 TargetTier: targetTier, 1654 MaxBondedAmt: maxBondedAmt, 1655 BondAsset: bondAsset, 1656 } 1657 err = c.dbCreateOrUpdateAccount(dc, ai) 1658 if err != nil { 1659 return nil, fmt.Errorf("failed to store account %v for dex %v: %w", 1660 dc.acct.id, dc.acct.host, err) 1661 } 1662 } 1663 1664 success = true // we're doing this 1665 1666 dc.acct.authMtx.Lock() 1667 dc.acct.pendingBonds = append(dc.acct.pendingBonds, dbBond) 1668 dc.acct.authMtx.Unlock() 1669 1670 if !acctExists { // *after* setting pendingBonds for rotateBonds accounting if targetTier>0 1671 c.addDexConnection(dc) 1672 // NOTE: it's still not authed if this was the first bond 1673 } 1674 1675 // Broadcast the bond and start waiting for confs. 1676 c.log.Infof("Broadcasting bond %v (%s) with lock time %v, data = %x.\n\n"+ 1677 "BACKUP refund tx paying to current wallet: %x\n\n", 1678 bondCoinStr, unbip(bond.AssetID), lockTime, bond.Data, bond.RedeemTx) 1679 if bondCoinCast, err := wallet.SendTransaction(bond.SignedTx); err != nil { 1680 c.log.Warnf("Failed to broadcast bond txn (%v). Tx bytes: %x", err, bond.SignedTx) 1681 // There is a good possibility it actually made it to the network. We 1682 // should start monitoring, perhaps even rebroadcast. It's tempting to 1683 // abort and remove the pending bond, but that's bad if it's sent. 1684 } else if !bytes.Equal(bond.CoinID, bondCoinCast) { 1685 c.log.Warnf("Broadcasted bond %v; was expecting %v!", 1686 coinIDString(bond.AssetID, bondCoinCast), bondCoinStr) 1687 } 1688 1689 // Set up the coin waiter, which watches confirmations so the user knows 1690 // when to expect their account to be marked paid by the server. 1691 c.monitorBondConfs(dc, bond, reqConfs) 1692 1693 c.updateAssetBalance(bond.AssetID) 1694 1695 // Start waiting for reqConfs. 1696 subject, details := c.formatDetails(TopicBondConfirming, reqConfs, makeCoinIDToken(bondCoinStr, bond.AssetID), unbip(bond.AssetID), dc.acct.host) 1697 c.notify(newBondPostNoteWithConfirmations(TopicBondConfirming, subject, 1698 details, db.Success, bond.AssetID, bondCoinStr, 0, dc.acct.host, c.exchangeAuth(dc))) 1699 1700 return bond.CoinID, nil 1701 } 1702 1703 func (c *Core) updatePendingBondConfs(dc *dexConnection, assetID uint32, coinID []byte, confs uint32) { 1704 dc.acct.authMtx.Lock() 1705 defer dc.acct.authMtx.Unlock() 1706 bondIDStr := coinIDString(assetID, coinID) 1707 dc.acct.pendingBondsConfs[bondIDStr] = confs 1708 } 1709 1710 func (c *Core) bondConfirmed(dc *dexConnection, assetID uint32, coinID []byte, pbr *msgjson.PostBondResult) error { 1711 bondIDStr := coinIDString(assetID, coinID) 1712 // Update dc.acct.{bonds,pendingBonds,tier} under authMtx lock. 1713 var foundPending, foundConfirmed bool 1714 dc.acct.authMtx.Lock() 1715 delete(dc.acct.pendingBondsConfs, bondIDStr) 1716 for i, bond := range dc.acct.pendingBonds { 1717 if bond.AssetID == assetID && bytes.Equal(bond.CoinID, coinID) { 1718 // Delete the bond from pendingBonds and move it to (active) bonds. 1719 dc.acct.pendingBonds = cutBond(dc.acct.pendingBonds, i) 1720 dc.acct.bonds = append(dc.acct.bonds, bond) 1721 bond.Confirmed = true // not necessary, just for consistency with slice membership 1722 foundPending = true 1723 break 1724 } 1725 } 1726 if !foundPending { 1727 for _, bond := range dc.acct.bonds { 1728 if bond.AssetID == assetID && bytes.Equal(bond.CoinID, coinID) { 1729 foundConfirmed = true 1730 break 1731 } 1732 } 1733 } 1734 1735 dc.acct.rep = *pbr.Reputation 1736 effectiveTier := dc.acct.rep.EffectiveTier() 1737 bondedTier := dc.acct.rep.BondedTier 1738 targetTier := dc.acct.targetTier 1739 isAuthed := dc.acct.isAuthed 1740 dc.acct.authMtx.Unlock() 1741 1742 if foundPending { 1743 // Set bond confirmed in the DB. 1744 err := c.db.ConfirmBond(dc.acct.host, assetID, coinID) 1745 if err != nil { 1746 return fmt.Errorf("db.ConfirmBond failure: %w", err) 1747 } 1748 subject, details := c.formatDetails(TopicBondConfirmed, effectiveTier, targetTier) 1749 c.notify(newBondPostNoteWithTier(TopicBondConfirmed, subject, details, db.Success, dc.acct.host, bondedTier, c.exchangeAuth(dc))) 1750 } else if !foundConfirmed { 1751 c.log.Errorf("bondConfirmed: Bond %s (%s) not found", bondIDStr, unbip(assetID)) 1752 // just try to authenticate... 1753 } // else already found confirmed (no-op) 1754 1755 // If we were not previously authenticated, we can infer that this was the 1756 // bond that created the account server-side, otherwise this was a top-up. 1757 if isAuthed { 1758 return nil // already logged in 1759 } 1760 1761 if dc.acct.locked() { 1762 c.log.Info("Login to check current account tier with newly confirmed bond %v.", bondIDStr) 1763 return nil 1764 } 1765 1766 err := c.authDEX(dc) 1767 if err != nil { 1768 subject, details := c.formatDetails(TopicDexAuthErrorBond, err) 1769 c.notify(newDEXAuthNote(TopicDexAuthError, subject, dc.acct.host, false, details, db.ErrorLevel)) 1770 return err 1771 } 1772 1773 subject, details := c.formatDetails(TopicAccountRegTier, effectiveTier) 1774 c.notify(newBondPostNoteWithTier(TopicAccountRegistered, subject, 1775 details, db.Success, dc.acct.host, bondedTier, c.exchangeAuth(dc))) // possibly redundant with SubjectBondConfirmed 1776 1777 return nil 1778 } 1779 1780 func (c *Core) bondExpired(dc *dexConnection, assetID uint32, coinID []byte, note *msgjson.BondExpiredNotification) error { 1781 // Update dc.acct.{bonds,tier} under authMtx lock. 1782 var found bool 1783 dc.acct.authMtx.Lock() 1784 for i, bond := range dc.acct.bonds { 1785 if bond.AssetID == assetID && bytes.Equal(bond.CoinID, coinID) { 1786 // Delete the bond from bonds and move it to expiredBonds. 1787 dc.acct.bonds = cutBond(dc.acct.bonds, i) 1788 if len(bond.RefundTx) > 0 || bond.KeyIndex != math.MaxUint32 { 1789 dc.acct.expiredBonds = append(dc.acct.expiredBonds, bond) // we'll wait for lockTime to pass to refund 1790 } else { 1791 c.log.Warnf("Dropping expired bond with no known keys or refund transaction. "+ 1792 "This was a placeholder for an unknown bond reported to use by the server. "+ 1793 "Bond ID: %x (%s)", coinIDString(bond.AssetID, bond.CoinID), unbip(bond.AssetID)) 1794 } 1795 found = true 1796 break 1797 } 1798 } 1799 if !found { // rotateBonds may have gotten to it first 1800 for _, bond := range dc.acct.expiredBonds { 1801 if bond.AssetID == assetID && bytes.Equal(bond.CoinID, coinID) { 1802 found = true 1803 break 1804 } 1805 } 1806 } 1807 1808 if note.Reputation != nil { 1809 dc.acct.rep = *note.Reputation 1810 } else { 1811 dc.acct.rep.BondedTier = note.Tier + int64(dc.acct.rep.Penalties) 1812 } 1813 targetTier := dc.acct.targetTier 1814 effectiveTier := dc.acct.rep.EffectiveTier() 1815 bondedTier := dc.acct.rep.BondedTier 1816 dc.acct.authMtx.Unlock() 1817 1818 bondIDStr := coinIDString(assetID, coinID) 1819 if !found { 1820 c.log.Warnf("bondExpired: Bond %s (%s) in bondexpired message not found locally (already refunded?).", 1821 bondIDStr, unbip(assetID)) 1822 } 1823 1824 if int64(targetTier) > effectiveTier { 1825 subject, details := c.formatDetails(TopicBondExpired, effectiveTier, targetTier) 1826 c.notify(newBondPostNoteWithTier(TopicBondExpired, subject, 1827 details, db.WarningLevel, dc.acct.host, bondedTier, c.exchangeAuth(dc))) 1828 } 1829 1830 return nil 1831 }