decred.org/dcrwallet/v3@v3.1.0/internal/vsp/vsp.go (about)

     1  package vsp
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"fmt"
     7  	"net"
     8  	"net/http"
     9  	"net/url"
    10  	"sync"
    11  
    12  	"decred.org/dcrwallet/v3/errors"
    13  	"decred.org/dcrwallet/v3/wallet"
    14  	"decred.org/dcrwallet/v3/wallet/udb"
    15  	"github.com/decred/dcrd/chaincfg/chainhash"
    16  	"github.com/decred/dcrd/dcrutil/v4"
    17  	"github.com/decred/dcrd/txscript/v4/stdaddr"
    18  	"github.com/decred/dcrd/wire"
    19  	vspd "github.com/decred/vspd/client/v3"
    20  )
    21  
    22  type DialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
    23  
    24  type Policy struct {
    25  	MaxFee     dcrutil.Amount
    26  	ChangeAcct uint32 // to derive fee addresses
    27  	FeeAcct    uint32 // to pay fees from, if inputs are not provided to Process
    28  }
    29  
    30  type Client struct {
    31  	wallet *wallet.Wallet
    32  	policy Policy
    33  	*vspd.Client
    34  
    35  	mu   sync.Mutex
    36  	jobs map[chainhash.Hash]*feePayment
    37  }
    38  
    39  type Config struct {
    40  	// URL specifies the base URL of the VSP
    41  	URL string
    42  
    43  	// PubKey specifies the VSP's base64 encoded public key
    44  	PubKey string
    45  
    46  	// Dialer specifies an optional dialer when connecting to the VSP.
    47  	Dialer DialFunc
    48  
    49  	// Wallet specifies a loaded wallet.
    50  	Wallet *wallet.Wallet
    51  
    52  	// Default policy for fee payments unless another is provided by the
    53  	// caller.
    54  	Policy Policy
    55  }
    56  
    57  func New(cfg Config) (*Client, error) {
    58  	u, err := url.Parse(cfg.URL)
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  	pubKey, err := base64.StdEncoding.DecodeString(cfg.PubKey)
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  	if cfg.Wallet == nil {
    67  		return nil, fmt.Errorf("wallet option not set")
    68  	}
    69  
    70  	client := &vspd.Client{
    71  		URL:    u.String(),
    72  		PubKey: pubKey,
    73  		Sign:   cfg.Wallet.SignMessage,
    74  		Log:    log,
    75  	}
    76  	client.Transport = &http.Transport{
    77  		DialContext: cfg.Dialer,
    78  	}
    79  
    80  	v := &Client{
    81  		wallet: cfg.Wallet,
    82  		policy: cfg.Policy,
    83  		Client: client,
    84  		jobs:   make(map[chainhash.Hash]*feePayment),
    85  	}
    86  	return v, nil
    87  }
    88  
    89  func (c *Client) FeePercentage(ctx context.Context) (float64, error) {
    90  	resp, err := c.Client.VspInfo(ctx)
    91  	if err != nil {
    92  		return -1, err
    93  	}
    94  	return resp.FeePercentage, nil
    95  }
    96  
    97  // ProcessUnprocessedTickets processes all tickets that don't currently have
    98  // any association with a VSP.
    99  func (c *Client) ProcessUnprocessedTickets(ctx context.Context) {
   100  	var wg sync.WaitGroup
   101  	c.wallet.ForUnspentUnexpiredTickets(ctx, func(hash *chainhash.Hash) error {
   102  		// Skip tickets which have a fee tx already associated with
   103  		// them; they are already processed by some vsp.
   104  		_, err := c.wallet.VSPFeeHashForTicket(ctx, hash)
   105  		if err == nil {
   106  			return nil
   107  		}
   108  		confirmed, err := c.wallet.IsVSPTicketConfirmed(ctx, hash)
   109  		if err != nil && !errors.Is(err, errors.NotExist) {
   110  			log.Error(err)
   111  			return nil
   112  		}
   113  
   114  		if confirmed {
   115  			return nil
   116  		}
   117  
   118  		c.mu.Lock()
   119  		fp := c.jobs[*hash]
   120  		c.mu.Unlock()
   121  		if fp != nil {
   122  			// Already processing this ticket with the VSP.
   123  			return nil
   124  		}
   125  
   126  		// Start processing in the background.
   127  		wg.Add(1)
   128  		go func() {
   129  			defer wg.Done()
   130  			err := c.Process(ctx, hash, nil)
   131  			if err != nil {
   132  				log.Error(err)
   133  			}
   134  		}()
   135  
   136  		return nil
   137  	})
   138  	wg.Wait()
   139  }
   140  
   141  // ProcessTicket attempts to process a given ticket based on the hash provided.
   142  func (c *Client) ProcessTicket(ctx context.Context, hash *chainhash.Hash) error {
   143  	err := c.Process(ctx, hash, nil)
   144  	if err != nil {
   145  		return err
   146  	}
   147  	return nil
   148  }
   149  
   150  // ProcessManagedTickets discovers tickets which were previously registered with
   151  // a VSP and begins syncing them in the background.  This is used to recover VSP
   152  // tracking after seed restores, and is only performed on unspent and unexpired
   153  // tickets.
   154  func (c *Client) ProcessManagedTickets(ctx context.Context) error {
   155  	err := c.wallet.ForUnspentUnexpiredTickets(ctx, func(hash *chainhash.Hash) error {
   156  		// We only want to process tickets that haven't been confirmed yet.
   157  		confirmed, err := c.wallet.IsVSPTicketConfirmed(ctx, hash)
   158  		if err != nil && !errors.Is(err, errors.NotExist) {
   159  			log.Error(err)
   160  			return nil
   161  		}
   162  		if confirmed {
   163  			return nil
   164  		}
   165  		c.mu.Lock()
   166  		_, ok := c.jobs[*hash]
   167  		c.mu.Unlock()
   168  		if ok {
   169  			// Already processing this ticket with the VSP.
   170  			return nil
   171  		}
   172  
   173  		// Make ticketstatus api call and only continue if ticket is
   174  		// found managed by this vsp.  The rest is the same codepath as
   175  		// for processing a new ticket.
   176  		status, err := c.status(ctx, hash)
   177  		if err != nil {
   178  			if errors.Is(err, errors.Locked) {
   179  				return err
   180  			}
   181  			return nil
   182  		}
   183  
   184  		if status.FeeTxStatus == "confirmed" {
   185  			feeHash, err := chainhash.NewHashFromStr(status.FeeTxHash)
   186  			if err != nil {
   187  				return err
   188  			}
   189  			err = c.wallet.UpdateVspTicketFeeToConfirmed(ctx, hash, feeHash, c.Client.URL, c.Client.PubKey)
   190  			if err != nil {
   191  				return err
   192  			}
   193  			return nil
   194  		} else if status.FeeTxHash != "" {
   195  			feeHash, err := chainhash.NewHashFromStr(status.FeeTxHash)
   196  			if err != nil {
   197  				return err
   198  			}
   199  			err = c.wallet.UpdateVspTicketFeeToPaid(ctx, hash, feeHash, c.Client.URL, c.Client.PubKey)
   200  			if err != nil {
   201  				return err
   202  			}
   203  			_ = c.feePayment(ctx, hash, true)
   204  		} else {
   205  			// Fee hasn't been paid at the provided VSP, so this should do that if needed.
   206  			_ = c.feePayment(ctx, hash, false)
   207  		}
   208  
   209  		return nil
   210  	})
   211  	return err
   212  }
   213  
   214  // Process begins processing a VSP fee payment for a ticket.  If feeTx contains
   215  // inputs, is used to pay the VSP fee.  Otherwise, new inputs are selected and
   216  // locked to prevent double spending the fee.
   217  //
   218  // feeTx must not be nil, but may point to an empty transaction, and is modified
   219  // with the inputs and the fee and change outputs before returning without an
   220  // error.  The fee transaction is also recorded as unpublised in the wallet, and
   221  // the fee hash is associated with the ticket.
   222  func (c *Client) Process(ctx context.Context, ticketHash *chainhash.Hash, feeTx *wire.MsgTx) error {
   223  	vspTicket, err := c.wallet.VSPTicketInfo(ctx, ticketHash)
   224  	if err != nil && !errors.Is(err, errors.NotExist) {
   225  		return err
   226  	}
   227  	feeStatus := udb.VSPFeeProcessStarted // Will be used if the ticket isn't registered to the vsp yet.
   228  	if vspTicket != nil {
   229  		feeStatus = udb.FeeStatus(vspTicket.FeeTxStatus)
   230  	}
   231  
   232  	switch feeStatus {
   233  	case udb.VSPFeeProcessStarted, udb.VSPFeeProcessErrored:
   234  		// If VSPTicket has been started or errored then attempt to create a new fee
   235  		// transaction, submit it then confirm.
   236  		fp := c.feePayment(ctx, ticketHash, false)
   237  		if fp == nil {
   238  			err := c.wallet.UpdateVspTicketFeeToErrored(ctx, ticketHash, c.Client.URL, c.Client.PubKey)
   239  			if err != nil {
   240  				return err
   241  			}
   242  			return fmt.Errorf("fee payment cannot be processed")
   243  		}
   244  		fp.mu.Lock()
   245  		if fp.feeTx == nil {
   246  			fp.feeTx = feeTx
   247  		}
   248  		fp.mu.Unlock()
   249  		err := fp.receiveFeeAddress()
   250  		if err != nil {
   251  			err := c.wallet.UpdateVspTicketFeeToErrored(ctx, ticketHash, c.Client.URL, c.Client.PubKey)
   252  			if err != nil {
   253  				return err
   254  			}
   255  			// XXX, retry? (old Process retried)
   256  			// but this may not be necessary any longer as the parent of
   257  			// the ticket is always relayed to the vsp as well.
   258  			return err
   259  		}
   260  		err = fp.makeFeeTx(feeTx)
   261  		if err != nil {
   262  			err := c.wallet.UpdateVspTicketFeeToErrored(ctx, ticketHash, c.Client.URL, c.Client.PubKey)
   263  			if err != nil {
   264  				return err
   265  			}
   266  			return err
   267  		}
   268  		return fp.submitPayment()
   269  	case udb.VSPFeeProcessPaid:
   270  		// If a VSP ticket has been paid, but confirm payment.
   271  		if len(vspTicket.Host) > 0 && vspTicket.Host != c.Client.URL {
   272  			// Cannot confirm a paid ticket that is already with another VSP.
   273  			return fmt.Errorf("ticket already paid or confirmed with another vsp")
   274  		}
   275  		fp := c.feePayment(ctx, ticketHash, true)
   276  		if fp == nil {
   277  			// Don't update VSPStatus to Errored if it was already paid or
   278  			// confirmed.
   279  			return fmt.Errorf("fee payment cannot be processed")
   280  		}
   281  
   282  		return fp.confirmPayment()
   283  	case udb.VSPFeeProcessConfirmed:
   284  		// VSPTicket has already been confirmed, there is nothing to process.
   285  		return nil
   286  	}
   287  	return nil
   288  }
   289  
   290  // SetVoteChoice takes the provided consensus, tspend and treasury key voting
   291  // preferences, and checks if they match the status of the specified ticket from
   292  // the connected VSP. The status provides the current voting preferences so we
   293  // can just update from there if need be.
   294  func (c *Client) SetVoteChoice(ctx context.Context, hash *chainhash.Hash,
   295  	choices map[string]string, tspendPolicy map[string]string, treasuryPolicy map[string]string) error {
   296  
   297  	// Retrieve current voting preferences from VSP.
   298  	status, err := c.status(ctx, hash)
   299  	if err != nil {
   300  		if errors.Is(err, errors.Locked) {
   301  			return err
   302  		}
   303  		log.Errorf("Could not check status of VSP ticket %s: %v", hash, err)
   304  		return nil
   305  	}
   306  
   307  	// Check for any mismatch between the provided voting preferences and the
   308  	// VSP preferences to determine if VSP needs to be updated.
   309  	update := false
   310  
   311  	// Check consensus vote choices.
   312  	for newAgenda, newChoice := range choices {
   313  		vspChoice, ok := status.VoteChoices[newAgenda]
   314  		if !ok {
   315  			update = true
   316  			break
   317  		}
   318  		if vspChoice != newChoice {
   319  			update = true
   320  			break
   321  		}
   322  	}
   323  
   324  	// Check tspend policies.
   325  	for newTSpend, newChoice := range tspendPolicy {
   326  		vspChoice, ok := status.TSpendPolicy[newTSpend]
   327  		if !ok {
   328  			update = true
   329  			break
   330  		}
   331  		if vspChoice != newChoice {
   332  			update = true
   333  			break
   334  		}
   335  	}
   336  
   337  	// Check treasury policies.
   338  	for newKey, newChoice := range treasuryPolicy {
   339  		vspChoice, ok := status.TSpendPolicy[newKey]
   340  		if !ok {
   341  			update = true
   342  			break
   343  		}
   344  		if vspChoice != newChoice {
   345  			update = true
   346  			break
   347  		}
   348  	}
   349  
   350  	if !update {
   351  		log.Debugf("VSP already has correct vote choices for ticket %s", hash)
   352  		return nil
   353  	}
   354  
   355  	log.Debugf("Updating vote choices on VSP for ticket %s", hash)
   356  	err = c.setVoteChoices(ctx, hash, choices, tspendPolicy, treasuryPolicy)
   357  	if err != nil {
   358  		return err
   359  	}
   360  	return nil
   361  }
   362  
   363  // TicketInfo stores per-ticket info tracked by a VSP Client instance.
   364  type TicketInfo struct {
   365  	TicketHash     chainhash.Hash
   366  	CommitmentAddr stdaddr.StakeAddress
   367  	VotingAddr     stdaddr.StakeAddress
   368  	State          uint32
   369  	Fee            dcrutil.Amount
   370  	FeeHash        chainhash.Hash
   371  
   372  	// TODO: include stuff returned by the status() call?
   373  }
   374  
   375  // TrackedTickets returns information about all outstanding tickets tracked by
   376  // a vsp.Client instance.
   377  //
   378  // Currently this returns only info about tickets which fee hasn't been paid or
   379  // confirmed at enough depth to be considered committed to.
   380  func (c *Client) TrackedTickets() []*TicketInfo {
   381  	// Collect all jobs first, to avoid working under two different locks.
   382  	c.mu.Lock()
   383  	jobs := make([]*feePayment, 0, len(c.jobs))
   384  	for _, job := range c.jobs {
   385  		jobs = append(jobs, job)
   386  	}
   387  	c.mu.Unlock()
   388  
   389  	tickets := make([]*TicketInfo, 0, len(jobs))
   390  	for _, job := range jobs {
   391  		job.mu.Lock()
   392  		tickets = append(tickets, &TicketInfo{
   393  			TicketHash:     job.ticketHash,
   394  			CommitmentAddr: job.commitmentAddr,
   395  			VotingAddr:     job.votingAddr,
   396  			State:          uint32(job.state),
   397  			Fee:            job.fee,
   398  			FeeHash:        job.feeHash,
   399  		})
   400  		job.mu.Unlock()
   401  	}
   402  
   403  	return tickets
   404  }