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  }