decred.org/dcrwallet/v3@v3.1.0/ticketbuyer/tb.go (about)

     1  // Copyright (c) 2018-2020 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package ticketbuyer
     6  
     7  import (
     8  	"context"
     9  	"net"
    10  	"runtime/trace"
    11  	"sync"
    12  
    13  	"decred.org/dcrwallet/v3/errors"
    14  	"decred.org/dcrwallet/v3/internal/vsp"
    15  	"decred.org/dcrwallet/v3/wallet"
    16  	"github.com/decred/dcrd/dcrutil/v4"
    17  	"github.com/decred/dcrd/txscript/v4/stdaddr"
    18  	"github.com/decred/dcrd/wire"
    19  )
    20  
    21  const minconf = 1
    22  
    23  // Config modifies the behavior of TB.
    24  type Config struct {
    25  	BuyTickets bool
    26  
    27  	// Account to buy tickets from
    28  	Account uint32
    29  
    30  	// Account to derive voting addresses from; overridden by VotingAddr
    31  	VotingAccount uint32
    32  
    33  	// Minimum amount to maintain in purchasing account
    34  	Maintain dcrutil.Amount
    35  
    36  	// Address to assign voting rights; overrides VotingAccount
    37  	VotingAddr stdaddr.StakeAddress
    38  
    39  	// Commitment address for stakepool fees
    40  	PoolFeeAddr stdaddr.StakeAddress
    41  
    42  	// Stakepool fee percentage (between 0-100)
    43  	PoolFees float64
    44  
    45  	// Limit maximum number of purchased tickets per block
    46  	Limit int
    47  
    48  	// CSPP-related options
    49  	CSPPServer         string
    50  	DialCSPPServer     func(ctx context.Context, network, addr string) (net.Conn, error)
    51  	MixedAccount       uint32
    52  	MixedAccountBranch uint32
    53  	TicketSplitAccount uint32
    54  	ChangeAccount      uint32
    55  	MixChange          bool
    56  
    57  	// VSP client
    58  	VSP *vsp.Client
    59  }
    60  
    61  // TB is an automated ticket buyer, buying as many tickets as possible given an
    62  // account's available balance.  TB may be configured to buy tickets for any
    63  // arbitrary voting address or (optional) stakepool.
    64  type TB struct {
    65  	wallet *wallet.Wallet
    66  
    67  	cfg Config
    68  	mu  sync.Mutex
    69  }
    70  
    71  // New returns a new TB to buy tickets from a wallet using the default config.
    72  func New(w *wallet.Wallet) *TB {
    73  	return &TB{wallet: w}
    74  }
    75  
    76  // Run executes the ticket buyer.  If the private passphrase is incorrect, or
    77  // ever becomes incorrect due to a wallet passphrase change, Run exits with an
    78  // errors.Passphrase error.
    79  func (tb *TB) Run(ctx context.Context, passphrase []byte) error {
    80  	if len(passphrase) > 0 {
    81  		err := tb.wallet.Unlock(ctx, passphrase, nil)
    82  		if err != nil {
    83  			return err
    84  		}
    85  	}
    86  
    87  	c := tb.wallet.NtfnServer.MainTipChangedNotifications()
    88  	defer c.Done()
    89  
    90  	ctx, outerCancel := context.WithCancel(ctx)
    91  	defer outerCancel()
    92  	var fatal error
    93  	var fatalMu sync.Mutex
    94  
    95  	var nextIntervalStart, expiry int32
    96  	var cancels []func()
    97  	for {
    98  		select {
    99  		case <-ctx.Done():
   100  			defer outerCancel()
   101  			fatalMu.Lock()
   102  			err := fatal
   103  			fatalMu.Unlock()
   104  			if err != nil {
   105  				return err
   106  			}
   107  			return ctx.Err()
   108  		case n := <-c.C:
   109  			if len(n.AttachedBlocks) == 0 {
   110  				continue
   111  			}
   112  
   113  			tip := n.AttachedBlocks[len(n.AttachedBlocks)-1]
   114  			w := tb.wallet
   115  
   116  			// Don't perform any actions while transactions are not synced through
   117  			// the tip block.
   118  			rp, err := w.RescanPoint(ctx)
   119  			if err != nil {
   120  				return err
   121  			}
   122  			if rp != nil {
   123  				log.Debugf("Skipping autobuyer actions: transactions are not synced")
   124  				continue
   125  			}
   126  
   127  			tipHeader, err := w.BlockHeader(ctx, tip)
   128  			if err != nil {
   129  				log.Error(err)
   130  				continue
   131  			}
   132  			height := int32(tipHeader.Height)
   133  
   134  			// Cancel any ongoing ticket purchases which are buying
   135  			// at an old ticket price or are no longer able to
   136  			// create mined tickets the window.
   137  			if height+2 >= nextIntervalStart {
   138  				for i, cancel := range cancels {
   139  					cancel()
   140  					cancels[i] = nil
   141  				}
   142  				cancels = cancels[:0]
   143  
   144  				intervalSize := int32(w.ChainParams().StakeDiffWindowSize)
   145  				currentInterval := height / intervalSize
   146  				nextIntervalStart = (currentInterval + 1) * intervalSize
   147  
   148  				// Skip this purchase when no more tickets may be purchased in the interval and
   149  				// the next sdiff is unknown.  The earliest any ticket may be mined is two
   150  				// blocks from now, with the next block containing the split transaction
   151  				// that the ticket purchase spends.
   152  				if height+2 == nextIntervalStart {
   153  					log.Debugf("Skipping purchase: next sdiff interval starts soon")
   154  					continue
   155  				}
   156  				// Set expiry to prevent tickets from being mined in the next
   157  				// sdiff interval.  When the next block begins the new interval,
   158  				// the ticket is being purchased for the next interval; therefore
   159  				// increment expiry by a full sdiff window size to prevent it
   160  				// being mined in the interval after the next.
   161  				expiry = nextIntervalStart
   162  				if height+1 == nextIntervalStart {
   163  					expiry += intervalSize
   164  				}
   165  			}
   166  
   167  			// Read config
   168  			tb.mu.Lock()
   169  			cfg := tb.cfg
   170  			tb.mu.Unlock()
   171  
   172  			multiple := 1
   173  			if cfg.CSPPServer != "" {
   174  				multiple = cfg.Limit
   175  				cfg.Limit = 1
   176  			}
   177  
   178  			cancelCtx, cancel := context.WithCancel(ctx)
   179  			cancels = append(cancels, cancel)
   180  			buyTickets := func() {
   181  				err := tb.buy(cancelCtx, passphrase, tipHeader, expiry, &cfg)
   182  				if err != nil {
   183  					switch {
   184  					// silence these errors
   185  					case errors.Is(err, errors.InsufficientBalance):
   186  					case errors.Is(err, context.Canceled):
   187  					case errors.Is(err, context.DeadlineExceeded):
   188  					default:
   189  						log.Errorf("Ticket purchasing failed: %v", err)
   190  					}
   191  					if errors.Is(err, errors.Passphrase) {
   192  						fatalMu.Lock()
   193  						fatal = err
   194  						fatalMu.Unlock()
   195  						outerCancel()
   196  					}
   197  				}
   198  			}
   199  			for i := 0; cfg.BuyTickets && i < multiple; i++ {
   200  				go buyTickets()
   201  			}
   202  			go func() {
   203  				err := tb.mixChange(ctx, &cfg)
   204  				if err != nil {
   205  					log.Error(err)
   206  				}
   207  			}()
   208  		}
   209  	}
   210  }
   211  
   212  func (tb *TB) buy(ctx context.Context, passphrase []byte, tip *wire.BlockHeader, expiry int32,
   213  	cfg *Config) error {
   214  	ctx, task := trace.NewTask(ctx, "ticketbuyer.buy")
   215  	defer task.End()
   216  
   217  	tb.mu.Lock()
   218  	buyTickets := tb.cfg.BuyTickets
   219  	tb.mu.Unlock()
   220  	if !buyTickets {
   221  		return nil
   222  	}
   223  
   224  	w := tb.wallet
   225  
   226  	// Unable to publish any transactions if the network backend is unset.
   227  	n, err := w.NetworkBackend()
   228  	if err != nil {
   229  		return err
   230  	}
   231  
   232  	if len(passphrase) > 0 {
   233  		// Ensure wallet is unlocked with the current passphrase.  If the passphase
   234  		// is changed, the Run exits and TB must be restarted with the new
   235  		// passphrase.
   236  		err = w.Unlock(ctx, passphrase, nil)
   237  		if err != nil {
   238  			return err
   239  		}
   240  	}
   241  
   242  	// Read config
   243  	account := cfg.Account
   244  	maintain := cfg.Maintain
   245  	votingAddr := cfg.VotingAddr
   246  	poolFeeAddr := cfg.PoolFeeAddr
   247  	poolFees := cfg.PoolFees
   248  	limit := cfg.Limit
   249  	csppServer := cfg.CSPPServer
   250  	dialCSPPServer := cfg.DialCSPPServer
   251  	votingAccount := cfg.VotingAccount
   252  	mixedAccount := cfg.MixedAccount
   253  	mixedBranch := cfg.MixedAccountBranch
   254  	splitAccount := cfg.TicketSplitAccount
   255  	changeAccount := cfg.ChangeAccount
   256  
   257  	sdiff, err := w.NextStakeDifficultyAfterHeader(ctx, tip)
   258  	if err != nil {
   259  		return err
   260  	}
   261  
   262  	// Determine how many tickets to buy
   263  	var buy int
   264  	if maintain != 0 {
   265  		bal, err := w.AccountBalance(ctx, account, minconf)
   266  		if err != nil {
   267  			return err
   268  		}
   269  		spendable := bal.Spendable
   270  		if spendable < maintain {
   271  			log.Debugf("Skipping purchase: low available balance")
   272  			return nil
   273  		}
   274  		spendable -= maintain
   275  		buy = int(spendable / sdiff)
   276  		if buy == 0 {
   277  			log.Debugf("Skipping purchase: low available balance")
   278  			return nil
   279  		}
   280  		max := int(w.ChainParams().MaxFreshStakePerBlock)
   281  		if buy > max {
   282  			buy = max
   283  		}
   284  	} else {
   285  		buy = int(w.ChainParams().MaxFreshStakePerBlock)
   286  	}
   287  	if limit == 0 && csppServer != "" {
   288  		buy = 1
   289  	} else if limit > 0 && buy > limit {
   290  		buy = limit
   291  	}
   292  
   293  	purchaseTicketReq := &wallet.PurchaseTicketsRequest{
   294  		Count:         buy,
   295  		SourceAccount: account,
   296  		VotingAddress: votingAddr,
   297  		MinConf:       minconf,
   298  		Expiry:        expiry,
   299  
   300  		// CSPP
   301  		CSPPServer:         csppServer,
   302  		DialCSPPServer:     dialCSPPServer,
   303  		VotingAccount:      votingAccount,
   304  		MixedAccount:       mixedAccount,
   305  		MixedAccountBranch: mixedBranch,
   306  		MixedSplitAccount:  splitAccount,
   307  		ChangeAccount:      changeAccount,
   308  
   309  		// VSPs
   310  		VSPAddress: poolFeeAddr,
   311  		VSPFees:    poolFees,
   312  	}
   313  	// If VSP is configured, we need to set the methods for vsp fee processment.
   314  	if tb.cfg.VSP != nil {
   315  		purchaseTicketReq.VSPFeePaymentProcess = tb.cfg.VSP.Process
   316  		purchaseTicketReq.VSPFeeProcess = tb.cfg.VSP.FeePercentage
   317  	}
   318  	tix, err := w.PurchaseTickets(ctx, n, purchaseTicketReq)
   319  	if tix != nil {
   320  		for _, hash := range tix.TicketHashes {
   321  			log.Infof("Purchased ticket %v at stake difficulty %v", hash, sdiff)
   322  		}
   323  	}
   324  	return err
   325  }
   326  
   327  // AccessConfig runs f with the current config passed as a parameter.  The
   328  // config is protected by a mutex and this function is safe for concurrent
   329  // access to read or modify the config.  It is unsafe to leak a pointer to the
   330  // config, but a copy of *cfg is legal.
   331  func (tb *TB) AccessConfig(f func(cfg *Config)) {
   332  	tb.mu.Lock()
   333  	f(&tb.cfg)
   334  	tb.mu.Unlock()
   335  }
   336  
   337  func (tb *TB) mixChange(ctx context.Context, cfg *Config) error {
   338  	// Read config
   339  	dial := cfg.DialCSPPServer
   340  	csppServer := cfg.CSPPServer
   341  	mixedAccount := cfg.MixedAccount
   342  	mixedBranch := cfg.MixedAccountBranch
   343  	changeAccount := cfg.ChangeAccount
   344  	mixChange := cfg.MixChange
   345  
   346  	if !mixChange || csppServer == "" {
   347  		return nil
   348  	}
   349  
   350  	ctx, task := trace.NewTask(ctx, "ticketbuyer.mixChange")
   351  	defer task.End()
   352  
   353  	return tb.wallet.MixAccount(ctx, dial, csppServer, changeAccount, mixedAccount, mixedBranch)
   354  }