decred.org/dcrdex@v1.0.3/client/core/bond.go (about)

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