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 }