From 9a84573fa864a25687639fa48ba46eb5fdbb403c Mon Sep 17 00:00:00 2001 From: jholdstock Date: Thu, 10 Aug 2023 10:21:04 +0100 Subject: [PATCH] vsp: Introduce VSPTicket struct. All of the fields necessary to register a ticket with a VSP are set upon creation of a VSPTicket struct. This helps to ensure no invalid tickets are ever added to a VSP client, and removes a lot of duplicated logic from feePayment. --- internal/rpc/jsonrpc/methods.go | 28 ++++- internal/rpc/rpcserver/server.go | 23 ++-- internal/vsp/feepayment.go | 178 ++++++++----------------------- internal/vsp/vsp.go | 48 +++++---- wallet/createtx.go | 17 ++- wallet/vspticket.go | 88 +++++++++++++++ wallet/wallet.go | 37 +++++-- 7 files changed, 239 insertions(+), 180 deletions(-) create mode 100644 wallet/vspticket.go diff --git a/internal/rpc/jsonrpc/methods.go b/internal/rpc/jsonrpc/methods.go index c43d25def..89b31a17d 100644 --- a/internal/rpc/jsonrpc/methods.go +++ b/internal/rpc/jsonrpc/methods.go @@ -3491,7 +3491,17 @@ func (s *Server) processUnmanagedTicket(ctx context.Context, icmd interface{}) ( return nil, err } - err = vspClient.Process(ctx, ticketHash, nil) + w, ok := s.walletLoader.LoadedWallet() + if !ok { + return nil, errUnloadedWallet + } + + ticket, err := w.NewVSPTicket(ctx, ticketHash) + if err != nil { + return nil, err + } + + err = vspClient.Process(ctx, ticket, nil) if err != nil { return nil, err } @@ -4671,7 +4681,13 @@ func (s *Server) updateVSPVoteChoices(ctx context.Context, w *wallet.Wallet, tic if err != nil { return err } - err = vspClient.SetVoteChoice(ctx, ticketHash, choices, tspendPolicy, treasuryPolicy) + + ticket, err := w.NewVSPTicket(ctx, ticketHash) + if err != nil { + return err + } + + err = vspClient.SetVoteChoice(ctx, ticket, choices, tspendPolicy, treasuryPolicy) return err } @@ -4688,9 +4704,15 @@ func (s *Server) updateVSPVoteChoices(ctx context.Context, w *wallet.Wallet, tic if err != nil { return err } + + ticket, err := w.NewVSPTicket(ctx, hash) + if err != nil { + return err + } + // Never return errors here, so all tickets are tried. // The first error will be returned to the user. - err = vspClient.SetVoteChoice(ctx, hash, choices, tspendPolicy, treasuryPolicy) + err = vspClient.SetVoteChoice(ctx, ticket, choices, tspendPolicy, treasuryPolicy) if err != nil { return err } diff --git a/internal/rpc/rpcserver/server.go b/internal/rpc/rpcserver/server.go index 142cf5940..6a934c48b 100644 --- a/internal/rpc/rpcserver/server.go +++ b/internal/rpc/rpcserver/server.go @@ -4161,14 +4161,21 @@ func (s *walletServer) SyncVSPFailedTickets(ctx context.Context, req *pb.SyncVSP return nil, status.Errorf(codes.Unknown, "TicketBuyerV3 instance failed to start. Error: %v", err) } - // process tickets fee if needed. + // Process tickets fee if needed. for _, ticketHash := range failedTicketsFee { + + // If it fails to process again, we log it and continue with + // the wallet start. + // Not sure we need to log here since it's already warned elsewhere + + t, err := s.wallet.NewVSPTicket(ctx, &ticketHash) + if err != nil { + continue + } feeTx := new(wire.MsgTx) - err := vspClient.Process(ctx, &ticketHash, feeTx) + err = vspClient.Process(ctx, t, feeTx) if err != nil { - // if it fails to process again, we log it and continue with - // the wallet start. - // Not sure we need to log here since it's already warned elsewhere + continue } } return &pb.SyncVSPTicketsResponse{}, nil @@ -4325,7 +4332,11 @@ func (s *walletServer) SetVspdVoteChoices(ctx context.Context, req *pb.SetVspdVo return err } if ticketHost == vspHost { - err = vspClient.SetVoteChoice(ctx, hash, choices, tSpendChoices, treasuryChoices) + ticket, err := s.wallet.NewVSPTicket(ctx, hash) + if err != nil { + return err + } + err = vspClient.SetVoteChoice(ctx, ticket, choices, tSpendChoices, treasuryChoices) if err != nil { return err } diff --git a/internal/vsp/feepayment.go b/internal/vsp/feepayment.go index b0a775e16..df5630c18 100644 --- a/internal/vsp/feepayment.go +++ b/internal/vsp/feepayment.go @@ -16,12 +16,11 @@ import ( "sync" "time" - "github.com/decred/dcrd/blockchain/stake/v5" + "decred.org/dcrwallet/v4/wallet" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/dcrutil/v4" "github.com/decred/dcrd/txscript/v4/stdaddr" - "github.com/decred/dcrd/txscript/v4/stdscript" "github.com/decred/dcrd/wire" "github.com/decred/vspd/types/v2" ) @@ -62,7 +61,6 @@ func randomDuration(d time.Duration) time.Duration { var ( errStopped = errors.New("fee processing stopped") - errNotSolo = errors.New("not a solo ticket") ) // A random amount of delay (between zero and these jitter constants) is added @@ -78,15 +76,12 @@ type feePayment struct { client *Client ctx context.Context - // Set at feepayment creation and never changes - ticketHash chainhash.Hash - commitmentAddr stdaddr.StakeAddress - votingAddr stdaddr.StakeAddress - policy *Policy + // Set at feePayment creation and never changes + ticket *wallet.VSPTicket + policy *Policy // Requires locking for all access outside of Client.feePayment mu sync.Mutex - votingKey string ticketLive int32 ticketExpires int32 fee dcrutil.Amount @@ -112,35 +107,10 @@ const ( TicketSpent ) -func parseTicket(ticket *wire.MsgTx, params *chaincfg.Params) ( - votingAddr, commitmentAddr stdaddr.StakeAddress, err error) { - fail := func(err error) (_, _ stdaddr.StakeAddress, _ error) { - return nil, nil, err - } - if !stake.IsSStx(ticket) { - return fail(fmt.Errorf("%v is not a ticket", ticket)) - } - _, addrs := stdscript.ExtractAddrs(ticket.TxOut[0].Version, ticket.TxOut[0].PkScript, params) - if len(addrs) != 1 { - return fail(fmt.Errorf("cannot parse voting addr")) - } - switch addr := addrs[0].(type) { - case stdaddr.StakeAddress: - votingAddr = addr - default: - return fail(fmt.Errorf("address cannot be used for voting rights: %v", err)) - } - commitmentAddr, err = stake.AddrFromSStxPkScrCommitment(ticket.TxOut[1].PkScript, params) - if err != nil { - return fail(fmt.Errorf("cannot parse commitment address: %w", err)) - } - return -} - // calcHeights checks if the ticket has been mined, and if so, sets the live // height and expiry height fields. Should be called with mutex already held. func (fp *feePayment) calcHeights() { - _, minedHeight, err := fp.client.wallet.TxBlock(fp.ctx, &fp.ticketHash) + _, minedHeight, err := fp.client.wallet.TxBlock(fp.ctx, fp.ticket.Hash()) if err != nil { // This is not expected to ever error, as the ticket has already been // fetched from the wallet at least one before this point is reached. @@ -180,7 +150,7 @@ func (fp *feePayment) liveHeight() int32 { func (fp *feePayment) ticketSpent() bool { ctx := fp.ctx - ticketOut := wire.OutPoint{Hash: fp.ticketHash, Index: 0, Tree: 1} + ticketOut := wire.OutPoint{Hash: *fp.ticket.Hash(), Index: 0, Tree: 1} _, _, err := fp.client.wallet.Spender(ctx, &ticketOut) return err == nil } @@ -215,15 +185,16 @@ func (fp *feePayment) removedExpiredOrSpent() bool { func (fp *feePayment) remove(reason string) { fp.stop() - fp.client.log.Infof("ticket %v is %s; removing from VSP client", &fp.ticketHash, reason) + fp.client.log.Infof("Ticket %v is %s; removing from VSP client", fp.ticket, reason) fp.client.mu.Lock() - delete(fp.client.jobs, fp.ticketHash) + delete(fp.client.jobs, *fp.ticket.Hash()) fp.client.mu.Unlock() } // feePayment returns an existing managed fee payment, or creates and begins // processing a fee payment for a ticket. -func (c *Client) feePayment(ctx context.Context, ticketHash *chainhash.Hash, paidConfirmed bool) (fp *feePayment) { +func (c *Client) feePayment(ctx context.Context, ticket *wallet.VSPTicket, paidConfirmed bool) (fp *feePayment) { + ticketHash := ticket.Hash() c.mu.Lock() fp = c.jobs[*ticketHash] c.mu.Unlock() @@ -254,11 +225,11 @@ func (c *Client) feePayment(ctx context.Context, ticketHash *chainhash.Hash, pai w := c.wallet fp = &feePayment{ - client: c, - ctx: context.Background(), - ticketHash: *ticketHash, - policy: c.policy, - params: c.params, + client: c, + ctx: context.Background(), + ticket: ticket, + policy: c.policy, + params: c.params, } // No VSP interaction is required for spent tickets. @@ -267,23 +238,6 @@ func (c *Client) feePayment(ctx context.Context, ticketHash *chainhash.Hash, pai return fp } - ticket, err := c.tx(ctx, ticketHash) - if err != nil { - fp.client.log.Warnf("no ticket found for %v", ticketHash) - return nil - } - - fp.votingAddr, fp.commitmentAddr, err = parseTicket(ticket, c.params) - if err != nil { - fp.client.log.Errorf("%v is not a ticket: %v", ticketHash, err) - return nil - } - // Try to access the voting key. - fp.votingKey, err = w.DumpWIFPrivateKey(ctx, fp.votingAddr) - if err != nil { - fp.client.log.Errorf("no voting key for ticket %v: %v", ticketHash, err) - return nil - } feeHash, err := w.VSPFeeHashForTicket(ctx, ticketHash) if err != nil { // caller must schedule next method, as paying the fee may @@ -343,7 +297,7 @@ func (fp *feePayment) schedule(name string, method func() error) { fp.timer = nil } if method != nil { - fp.client.log.Debugf("scheduling %q for ticket %s in %v", name, &fp.ticketHash, delay) + fp.client.log.Debugf("Scheduling %q for ticket %s in %v", name, fp.ticket, delay) fp.timer = time.AfterFunc(delay, fp.task(name, method)) } } @@ -384,7 +338,7 @@ func (fp *feePayment) task(name string, method func() error) func() { fp.err = err fp.mu.Unlock() if err != nil { - fp.client.log.Errorf("ticket %v: %v: %v", &fp.ticketHash, name, err) + fp.client.log.Errorf("Ticket %v: %v: %v", fp.ticket, name, err) } } } @@ -404,7 +358,7 @@ func (fp *feePayment) receiveFeeAddress() error { // Fetch ticket and its parent transaction (typically, a split // transaction). - ticket, err := fp.client.tx(ctx, &fp.ticketHash) + ticket, err := fp.client.tx(ctx, fp.ticket.Hash()) if err != nil { return fmt.Errorf("failed to retrieve ticket: %w", err) } @@ -426,12 +380,12 @@ func (fp *feePayment) receiveFeeAddress() error { req := types.FeeAddressRequest{ Timestamp: time.Now().Unix(), - TicketHash: fp.ticketHash.String(), + TicketHash: fp.ticket.Hash().String(), TicketHex: ticketHex, ParentHex: parentHex, } - resp, err := fp.client.FeeAddress(ctx, req, fp.commitmentAddr) + resp, err := fp.client.FeeAddress(ctx, req, fp.ticket.CommitmentAddr()) if err != nil { return err } @@ -509,11 +463,11 @@ func (fp *feePayment) makeFeeTx(tx *wire.MsgTx) error { err := w.CreateVspPayment(ctx, tx, fee, feeAddr, fp.policy.FeeAcct, fp.policy.ChangeAcct) if err != nil { - return fmt.Errorf("unable to create VSP fee tx for ticket %v: %w", fp.ticketHash, err) + return fmt.Errorf("unable to create VSP fee tx for ticket %v: %w", fp.ticket, err) } feeHash := tx.TxHash() - err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey) + err = w.UpdateVspTicketFeeToPaid(ctx, fp.ticket.Hash(), &feeHash, fp.client.URL, fp.client.PubKey) if err != nil { return err } @@ -527,29 +481,13 @@ func (fp *feePayment) makeFeeTx(tx *wire.MsgTx) error { return nil } -func (c *Client) status(ctx context.Context, ticketHash *chainhash.Hash) (*types.TicketStatusResponse, error) { - ticketTx, err := c.tx(ctx, ticketHash) - if err != nil { - return nil, fmt.Errorf("failed to retrieve ticket %v: %w", ticketHash, err) - } - if len(ticketTx.TxOut) != 3 { - return nil, fmt.Errorf("ticket %v has multiple commitments: %w", ticketHash, errNotSolo) - } - - if !stake.IsSStx(ticketTx) { - return nil, fmt.Errorf("%v is not a ticket", ticketHash) - } - commitmentAddr, err := stake.AddrFromSStxPkScrCommitment(ticketTx.TxOut[1].PkScript, c.params) - if err != nil { - return nil, fmt.Errorf("failed to extract commitment address from %v: %w", - ticketHash, err) - } +func (c *Client) status(ctx context.Context, ticket *wallet.VSPTicket) (*types.TicketStatusResponse, error) { req := types.TicketStatusRequest{ - TicketHash: ticketHash.String(), + TicketHash: ticket.Hash().String(), } - resp, err := c.Client.TicketStatus(ctx, req, commitmentAddr) + resp, err := c.Client.TicketStatus(ctx, req, ticket.CommitmentAddr()) if err != nil { return nil, err } @@ -559,36 +497,18 @@ func (c *Client) status(ctx context.Context, ticketHash *chainhash.Hash) (*types return resp, nil } -func (c *Client) setVoteChoices(ctx context.Context, ticketHash *chainhash.Hash, +func (c *Client) setVoteChoices(ctx context.Context, ticket *wallet.VSPTicket, choices map[string]string, tspendPolicy map[string]string, treasuryPolicy map[string]string) error { - ticketTx, err := c.tx(ctx, ticketHash) - if err != nil { - return fmt.Errorf("failed to retrieve ticket %v: %w", ticketHash, err) - } - - if !stake.IsSStx(ticketTx) { - return fmt.Errorf("%v is not a ticket", ticketHash) - } - if len(ticketTx.TxOut) != 3 { - return fmt.Errorf("ticket %v has multiple commitments: %w", ticketHash, errNotSolo) - } - - commitmentAddr, err := stake.AddrFromSStxPkScrCommitment(ticketTx.TxOut[1].PkScript, c.params) - if err != nil { - return fmt.Errorf("failed to extract commitment address from %v: %w", - ticketHash, err) - } - req := types.SetVoteChoicesRequest{ Timestamp: time.Now().Unix(), - TicketHash: ticketHash.String(), + TicketHash: ticket.Hash().String(), VoteChoices: choices, TSpendPolicy: tspendPolicy, TreasuryPolicy: treasuryPolicy, } - _, err = c.Client.SetVoteChoices(ctx, req, commitmentAddr) + _, err := c.Client.SetVoteChoices(ctx, req, ticket.CommitmentAddr()) if err != nil { return err } @@ -652,13 +572,13 @@ func (fp *feePayment) reconcilePayment() error { if err != nil { return err } - err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey) + err = w.UpdateVspTicketFeeToPaid(ctx, fp.ticket.Hash(), &feeHash, fp.client.URL, fp.client.PubKey) if err != nil { return err } err = nil case types.ErrInvalidFeeTx, types.ErrCannotBroadcastFee: - err := w.UpdateVspTicketFeeToErrored(ctx, &fp.ticketHash, fp.client.URL, fp.client.PubKey) + err := w.UpdateVspTicketFeeToErrored(ctx, fp.ticket.Hash(), fp.client.URL, fp.client.PubKey) if err != nil { return err } @@ -676,7 +596,7 @@ func (fp *feePayment) reconcilePayment() error { return err } - err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey) + err = w.UpdateVspTicketFeeToPaid(ctx, fp.ticket.Hash(), &feeHash, fp.client.URL, fp.client.PubKey) if err != nil { return err } @@ -704,7 +624,6 @@ func (fp *feePayment) submitPayment() (err error) { // submitting a payment requires the fee tx to already be created. fp.mu.Lock() feeTx := fp.feeTx - votingKey := fp.votingKey fp.mu.Unlock() if feeTx == nil { feeTx = new(wire.MsgTx) @@ -715,18 +634,9 @@ func (fp *feePayment) submitPayment() (err error) { return err } } - if votingKey == "" { - votingKey, err = w.DumpWIFPrivateKey(ctx, fp.votingAddr) - if err != nil { - return err - } - fp.mu.Lock() - fp.votingKey = votingKey - fp.mu.Unlock() - } // Retrieve voting preferences - voteChoices, _, err := w.AgendaChoices(ctx, &fp.ticketHash) + voteChoices, _, err := w.AgendaChoices(ctx, fp.ticket.Hash()) if err != nil { return err } @@ -738,15 +648,15 @@ func (fp *feePayment) submitPayment() (err error) { req := types.PayFeeRequest{ Timestamp: time.Now().Unix(), - TicketHash: fp.ticketHash.String(), + TicketHash: fp.ticket.Hash().String(), FeeTx: feeTxHex, - VotingKey: votingKey, + VotingKey: fp.ticket.VotingKey(), VoteChoices: voteChoices, - TSpendPolicy: w.TSpendPolicyForTicket(&fp.ticketHash), - TreasuryPolicy: w.TreasuryKeyPolicyForTicket(&fp.ticketHash), + TSpendPolicy: w.TSpendPolicyForTicket(fp.ticket.Hash()), + TreasuryPolicy: w.TreasuryKeyPolicyForTicket(fp.ticket.Hash()), } - _, err = fp.client.PayFee(ctx, req, fp.commitmentAddr) + _, err = fp.client.PayFee(ctx, req, fp.ticket.CommitmentAddr()) if err != nil { var apiErr types.ErrorResponse if errors.As(err, &apiErr) && apiErr.Code == types.ErrFeeExpired { @@ -766,7 +676,7 @@ func (fp *feePayment) submitPayment() (err error) { // TODO - validate server timestamp? - fp.client.log.Infof("successfully processed %v", fp.ticketHash) + fp.client.log.Infof("successfully processed %v", fp.ticket) return nil } @@ -790,9 +700,9 @@ func (fp *feePayment) confirmPayment() (err error) { } }() - status, err := fp.client.status(ctx, &fp.ticketHash) + status, err := fp.client.status(ctx, fp.ticket) if err != nil { - fp.client.log.Warnf("Rescheduling status check for %v: %v", &fp.ticketHash, err) + fp.client.log.Warnf("Rescheduling status check for %v: %v", fp.ticket, err) fp.schedule("confirm payment", fp.confirmPayment) return nil } @@ -804,7 +714,7 @@ func (fp *feePayment) confirmPayment() (err error) { fp.schedule("confirm payment", fp.confirmPayment) return nil case "broadcast": - fp.client.log.Infof("VSP has successfully sent the fee tx for %v", &fp.ticketHash) + fp.client.log.Infof("VSP has successfully sent the fee tx for %v", fp.ticket) // Broadcasted, but not confirmed. fp.schedule("confirm payment", fp.confirmPayment) return nil @@ -814,20 +724,20 @@ func (fp *feePayment) confirmPayment() (err error) { fp.mu.Lock() feeHash := fp.feeHash fp.mu.Unlock() - err = w.UpdateVspTicketFeeToConfirmed(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey) + err = w.UpdateVspTicketFeeToConfirmed(ctx, fp.ticket.Hash(), &feeHash, fp.client.URL, fp.client.PubKey) if err != nil { return err } return nil case "error": fp.client.log.Warnf("VSP failed to broadcast feetx for %v -- restarting payment", - &fp.ticketHash) + fp.ticket) fp.schedule("reconcile payment", fp.reconcilePayment) return nil default: // XXX put in unknown state fp.client.log.Warnf("VSP responded with unknown FeeTxStatus %q for %v", - status.FeeTxStatus, &fp.ticketHash) + status.FeeTxStatus, fp.ticket) } return nil diff --git a/internal/vsp/vsp.go b/internal/vsp/vsp.go index b979c7a1f..fca52c438 100644 --- a/internal/vsp/vsp.go +++ b/internal/vsp/vsp.go @@ -113,12 +113,12 @@ func (c *Client) FeePercentage(ctx context.Context) (float64, error) { // ProcessUnprocessedTickets adds the provided tickets to the client. Noop if // a given ticket is already added. -func (c *Client) ProcessUnprocessedTickets(ctx context.Context, tickets []*chainhash.Hash) { +func (c *Client) ProcessUnprocessedTickets(ctx context.Context, tickets []*wallet.VSPTicket) { var wg sync.WaitGroup - for _, hash := range tickets { + for _, ticket := range tickets { c.mu.Lock() - fp := c.jobs[*hash] + fp := c.jobs[*ticket.Hash()] c.mu.Unlock() if fp != nil { // Already processing this ticket with the VSP. @@ -127,13 +127,13 @@ func (c *Client) ProcessUnprocessedTickets(ctx context.Context, tickets []*chain // Start processing in the background. wg.Add(1) - go func(ticketHash *chainhash.Hash) { + go func(t *wallet.VSPTicket) { defer wg.Done() - err := c.Process(ctx, ticketHash, nil) + err := c.Process(ctx, t, nil) if err != nil { c.log.Error(err) } - }(hash) + }(ticket) } wg.Wait() @@ -143,8 +143,9 @@ func (c *Client) ProcessUnprocessedTickets(ctx context.Context, tickets []*chain // their fee payment process. Noop if a given ticket is already added, or if the // ticket is not registered with the VSP. This is used to recover VSP tracking // after seed restores. -func (c *Client) ProcessManagedTickets(ctx context.Context, tickets []*chainhash.Hash) error { - for _, hash := range tickets { +func (c *Client) ProcessManagedTickets(ctx context.Context, tickets []*wallet.VSPTicket) error { + for _, ticket := range tickets { + hash := ticket.Hash() c.mu.Lock() _, ok := c.jobs[*hash] c.mu.Unlock() @@ -156,7 +157,7 @@ func (c *Client) ProcessManagedTickets(ctx context.Context, tickets []*chainhash // Make ticketstatus api call and only continue if ticket is // found managed by this vsp. The rest is the same codepath as // for processing a new ticket. - status, err := c.status(ctx, hash) + status, err := c.status(ctx, ticket) if err != nil { if errors.Is(err, errors.Locked) { return err @@ -183,10 +184,10 @@ func (c *Client) ProcessManagedTickets(ctx context.Context, tickets []*chainhash if err != nil { return err } - _ = c.feePayment(ctx, hash, true) + _ = c.feePayment(ctx, ticket, true) } else { // Fee hasn't been paid at the provided VSP, so this should do that if needed. - _ = c.feePayment(ctx, hash, false) + _ = c.feePayment(ctx, ticket, false) } } @@ -202,7 +203,8 @@ func (c *Client) ProcessManagedTickets(ctx context.Context, tickets []*chainhash // the inputs and the fee and change outputs before returning without an error. // The fee transaction is also recorded as unpublised in the wallet, and the fee // hash is associated with the ticket. -func (c *Client) Process(ctx context.Context, ticketHash *chainhash.Hash, feeTx *wire.MsgTx) error { +func (c *Client) Process(ctx context.Context, ticket *wallet.VSPTicket, feeTx *wire.MsgTx) error { + ticketHash := ticket.Hash() vspTicket, err := c.wallet.VSPTicketInfo(ctx, ticketHash) if err != nil && !errors.Is(err, errors.NotExist) { return err @@ -216,7 +218,7 @@ func (c *Client) Process(ctx context.Context, ticketHash *chainhash.Hash, feeTx case udb.VSPFeeProcessStarted, udb.VSPFeeProcessErrored: // If VSPTicket has been started or errored then attempt to create a new fee // transaction, submit it then confirm. - fp := c.feePayment(ctx, ticketHash, false) + fp := c.feePayment(ctx, ticket, false) if fp == nil { err := c.wallet.UpdateVspTicketFeeToErrored(ctx, ticketHash, c.Client.URL, c.Client.PubKey) if err != nil { @@ -255,7 +257,7 @@ func (c *Client) Process(ctx context.Context, ticketHash *chainhash.Hash, feeTx // Cannot confirm a paid ticket that is already with another VSP. return fmt.Errorf("ticket already paid or confirmed with another vsp") } - fp := c.feePayment(ctx, ticketHash, true) + fp := c.feePayment(ctx, ticket, true) if fp == nil { // Don't update VSPStatus to Errored if it was already paid or // confirmed. @@ -274,16 +276,16 @@ func (c *Client) Process(ctx context.Context, ticketHash *chainhash.Hash, feeTx // preferences, and checks if they match the status of the specified ticket from // the connected VSP. The status provides the current voting preferences so we // can just update from there if need be. -func (c *Client) SetVoteChoice(ctx context.Context, hash *chainhash.Hash, +func (c *Client) SetVoteChoice(ctx context.Context, ticket *wallet.VSPTicket, choices map[string]string, tspendPolicy map[string]string, treasuryPolicy map[string]string) error { // Retrieve current voting preferences from VSP. - status, err := c.status(ctx, hash) + status, err := c.status(ctx, ticket) if err != nil { if errors.Is(err, errors.Locked) { return err } - c.log.Errorf("Could not check status of VSP ticket %s: %v", hash, err) + c.log.Errorf("Could not check status of VSP ticket %s: %v", ticket, err) return nil } @@ -331,12 +333,12 @@ func (c *Client) SetVoteChoice(ctx context.Context, hash *chainhash.Hash, } if !update { - c.log.Debugf("VSP already has correct vote choices for ticket %s", hash) + c.log.Debugf("VSP already has correct vote choices for ticket %s", ticket) return nil } - c.log.Debugf("Updating vote choices on VSP for ticket %s", hash) - err = c.setVoteChoices(ctx, hash, choices, tspendPolicy, treasuryPolicy) + c.log.Debugf("Updating vote choices on VSP for ticket %s", ticket) + err = c.setVoteChoices(ctx, ticket, choices, tspendPolicy, treasuryPolicy) if err != nil { return err } @@ -373,9 +375,9 @@ func (c *Client) TrackedTickets() []*TicketInfo { for _, job := range jobs { job.mu.Lock() tickets = append(tickets, &TicketInfo{ - TicketHash: job.ticketHash, - CommitmentAddr: job.commitmentAddr, - VotingAddr: job.votingAddr, + TicketHash: *job.ticket.Hash(), + CommitmentAddr: job.ticket.CommitmentAddr(), + VotingAddr: job.ticket.VotingAddr(), State: job.state, Fee: job.fee, FeeHash: job.feeHash, diff --git a/wallet/createtx.go b/wallet/createtx.go index 3f36fe25a..731370a8e 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -1810,13 +1810,24 @@ func (w *Wallet) purchaseTickets(ctx context.Context, op errors.Op, feeTx.AddTxIn(wire.NewTxIn(&in.OutPoint, in.PrevOut.Value, nil)) } ticketHash := purchaseTicketsResponse.TicketHashes[i] - err = req.VSPFeePaymentProcess(ctx, ticketHash, feeTx) - if err != nil { - // unlock outpoints in case of error + + // Unlock outpoints in case of error. + unlock := func() { for _, outpoint := range vspFeeCredits[i] { w.UnlockOutpoint(&outpoint.OutPoint.Hash, outpoint.OutPoint.Index) } + } + + ticket, err := w.NewVSPTicket(ctx, ticketHash) + if err != nil { + unlock() + continue + } + + err = req.VSPFeePaymentProcess(ctx, ticket, feeTx) + if err != nil { + unlock() continue } // watch for outpoints change. diff --git a/wallet/vspticket.go b/wallet/vspticket.go new file mode 100644 index 000000000..6562847ba --- /dev/null +++ b/wallet/vspticket.go @@ -0,0 +1,88 @@ +// Copyright (c) 2023 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package wallet + +import ( + "context" + "fmt" + + "github.com/decred/dcrd/blockchain/stake/v5" + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/txscript/v4/stdaddr" + "github.com/decred/dcrd/txscript/v4/stdscript" +) + +type VSPTicket struct { + // Fields set during creation and never change. + hash *chainhash.Hash + commitmentAddr stdaddr.StakeAddress + votingAddr stdaddr.StakeAddress + votingKey string +} + +// NewVSPTicket ensures the provided hash refers to a ticket with exactly 3 +// outputs. It returns a VSPTicket instance containing all of the information +// necessary to register the ticket with a VSP. +func (w *Wallet) NewVSPTicket(ctx context.Context, hash *chainhash.Hash) (*VSPTicket, error) { + + txs, _, err := w.GetTransactionsByHashes(ctx, []*chainhash.Hash{hash}) + if err != nil { + return nil, err + } + + ticketTx := txs[0] + + if !stake.IsSStx(ticketTx) { + return nil, fmt.Errorf("%v is not a ticket", hash) + } + + if len(ticketTx.TxOut) != 3 { + return nil, fmt.Errorf("ticket %v has multiple commitments", hash) + } + + _, addrs := stdscript.ExtractAddrs(ticketTx.TxOut[0].Version, ticketTx.TxOut[0].PkScript, w.chainParams) + if len(addrs) != 1 { + return nil, fmt.Errorf("cannot parse voting addr for ticket %v", hash) + } + + ticket := &VSPTicket{ + hash: hash, + } + + switch addr := addrs[0].(type) { + case stdaddr.StakeAddress: + ticket.votingAddr = addr + default: + return nil, fmt.Errorf("address cannot be used for voting rights: %v", err) + } + + ticket.commitmentAddr, err = stake.AddrFromSStxPkScrCommitment(ticketTx.TxOut[1].PkScript, w.chainParams) + if err != nil { + return nil, fmt.Errorf("cannot parse commitment address: %w", err) + } + + ticket.votingKey, err = w.DumpWIFPrivateKey(ctx, ticket.votingAddr) + if err != nil { + return nil, err + } + + return ticket, nil +} + +func (v *VSPTicket) String() string { + return v.hash.String() +} +func (v *VSPTicket) Hash() *chainhash.Hash { + return v.hash +} +func (v *VSPTicket) CommitmentAddr() stdaddr.StakeAddress { + return v.commitmentAddr +} +func (v *VSPTicket) VotingAddr() stdaddr.StakeAddress { + return v.votingAddr +} +func (v *VSPTicket) VotingKey() string { + return v.votingKey +} diff --git a/wallet/wallet.go b/wallet/wallet.go index a97823901..ec1c97a16 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -1620,7 +1620,7 @@ type PurchaseTicketsRequest struct { // VSPFeePaymentProcess checks the fee payment status for the specified // ticket and, if necessary, starts a long-lived handler to process the fee // payment. - VSPFeePaymentProcess func(context.Context, *chainhash.Hash, *wire.MsgTx) error + VSPFeePaymentProcess func(context.Context, *VSPTicket, *wire.MsgTx) error // extraSplitOutput is an additional transaction output created during // UTXO contention, to be used as the input to pay a VSP fee @@ -5728,7 +5728,7 @@ func (w *Wallet) SetPublished(ctx context.Context, hash *chainhash.Hash, publish return nil } -type VSPTicket struct { +type TicketInfo struct { FeeHash chainhash.Hash FeeTxStatus uint32 VSPHostID uint32 @@ -5737,7 +5737,7 @@ type VSPTicket struct { } // VSPTicketInfo returns the various information for a given vsp ticket -func (w *Wallet) VSPTicketInfo(ctx context.Context, ticketHash *chainhash.Hash) (*VSPTicket, error) { +func (w *Wallet) VSPTicketInfo(ctx context.Context, ticketHash *chainhash.Hash) (*TicketInfo, error) { var data *udb.VSPTicket err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { var err error @@ -5753,7 +5753,7 @@ func (w *Wallet) VSPTicketInfo(ctx context.Context, ticketHash *chainhash.Hash) } else if data == nil { return nil, err } - convertedData := &VSPTicket{ + convertedData := &TicketInfo{ FeeHash: data.FeeHash, FeeTxStatus: data.FeeTxStatus, VSPHostID: data.VSPHostID, @@ -5925,8 +5925,8 @@ func (w *Wallet) ForUnspentUnexpiredTickets(ctx context.Context, // UnprocessedTickets returns the hash of every live/immature ticket in the // wallet database which is not currently being processed by a VSP. -func (w *Wallet) UnprocessedTickets(ctx context.Context) ([]*chainhash.Hash, error) { - unmanagedTickets := make([]*chainhash.Hash, 0) +func (w *Wallet) UnprocessedTickets(ctx context.Context) ([]*VSPTicket, error) { + hashes := make([]*chainhash.Hash, 0) err := w.ForUnspentUnexpiredTickets(ctx, func(hash *chainhash.Hash) error { // Skip tickets which have a fee tx already associated with // them; they are already processed by some vsp. @@ -5949,20 +5949,28 @@ func (w *Wallet) UnprocessedTickets(ctx context.Context) ([]*chainhash.Hash, err return nil } - unmanagedTickets = append(unmanagedTickets, hash) + hashes = append(hashes, hash) return nil }) if err != nil { return nil, err } + unmanagedTickets := make([]*VSPTicket, len(hashes)) + for i, hash := range hashes { + unmanagedTickets[i], err = w.NewVSPTicket(ctx, hash) + if err != nil { + return nil, err + } + } + return unmanagedTickets, nil } // ProcessedTickets returns the hash of every live/immature ticket in the wallet // database which is currently being processed by a VSP but isnt confirmed yet. -func (w *Wallet) ProcessedTickets(ctx context.Context) ([]*chainhash.Hash, error) { - managedTickets := make([]*chainhash.Hash, 0) +func (w *Wallet) ProcessedTickets(ctx context.Context) ([]*VSPTicket, error) { + hashes := make([]*chainhash.Hash, 0) err := w.ForUnspentUnexpiredTickets(ctx, func(hash *chainhash.Hash) error { // We only want to process tickets that haven't been confirmed yet. confirmed, err := w.IsVSPTicketConfirmed(ctx, hash) @@ -5978,13 +5986,20 @@ func (w *Wallet) ProcessedTickets(ctx context.Context) ([]*chainhash.Hash, error return nil } - managedTickets = append(managedTickets, hash) + hashes = append(hashes, hash) return nil }) - if err != nil { return nil, err } + managedTickets := make([]*VSPTicket, len(hashes)) + for i, hash := range hashes { + managedTickets[i], err = w.NewVSPTicket(ctx, hash) + if err != nil { + return nil, err + } + } + return managedTickets, nil }