decred.org/dcrwallet/v3@v3.1.0/internal/vsp/feepayment.go (about) 1 package vsp 2 3 import ( 4 "bytes" 5 "context" 6 cryptorand "crypto/rand" 7 "encoding/hex" 8 "fmt" 9 "sync" 10 "time" 11 12 "decred.org/dcrwallet/v3/errors" 13 "decred.org/dcrwallet/v3/internal/uniformprng" 14 "decred.org/dcrwallet/v3/wallet" 15 "decred.org/dcrwallet/v3/wallet/txrules" 16 "decred.org/dcrwallet/v3/wallet/txsizes" 17 "github.com/decred/dcrd/blockchain/stake/v5" 18 "github.com/decred/dcrd/chaincfg/chainhash" 19 "github.com/decred/dcrd/chaincfg/v3" 20 "github.com/decred/dcrd/dcrutil/v4" 21 "github.com/decred/dcrd/txscript/v4" 22 "github.com/decred/dcrd/txscript/v4/stdaddr" 23 "github.com/decred/dcrd/txscript/v4/stdscript" 24 "github.com/decred/dcrd/wire" 25 "github.com/decred/vspd/types/v2" 26 ) 27 28 var prng lockedRand 29 30 type lockedRand struct { 31 mu sync.Mutex 32 rand *uniformprng.Source 33 } 34 35 func (r *lockedRand) int63n(n int64) int64 { 36 r.mu.Lock() 37 defer r.mu.Unlock() 38 return r.rand.Int63n(n) 39 } 40 41 // duration returns a random time.Duration in [0,d) with uniform distribution. 42 func (r *lockedRand) duration(d time.Duration) time.Duration { 43 return time.Duration(r.int63n(int64(d))) 44 } 45 46 func (r *lockedRand) coinflip() bool { 47 r.mu.Lock() 48 defer r.mu.Unlock() 49 return r.rand.Uint32n(2) == 0 50 } 51 52 func init() { 53 source, err := uniformprng.RandSource(cryptorand.Reader) 54 if err != nil { 55 panic(err) 56 } 57 prng = lockedRand{ 58 rand: source, 59 } 60 } 61 62 var ( 63 errStopped = errors.New("fee processing stopped") 64 errNotSolo = errors.New("not a solo ticket") 65 ) 66 67 // A random amount of delay (between zero and these jitter constants) is added 68 // before performing some background action with the VSP. The delay is reduced 69 // when a ticket is currently live, as it may be called to vote any time. 70 const ( 71 immatureJitter = time.Hour 72 liveJitter = 5 * time.Minute 73 unminedJitter = 2 * time.Minute 74 ) 75 76 type feePayment struct { 77 client *Client 78 ctx context.Context 79 80 // Set at feepayment creation and never changes 81 ticketHash chainhash.Hash 82 commitmentAddr stdaddr.StakeAddress 83 votingAddr stdaddr.StakeAddress 84 policy Policy 85 86 // Requires locking for all access outside of Client.feePayment 87 mu sync.Mutex 88 votingKey string 89 ticketLive int32 90 ticketExpires int32 91 fee dcrutil.Amount 92 feeAddr stdaddr.Address 93 feeHash chainhash.Hash 94 feeTx *wire.MsgTx 95 state state 96 err error 97 98 timerMu sync.Mutex 99 timer *time.Timer 100 } 101 102 type state uint32 103 104 const ( 105 _ state = iota 106 unprocessed 107 feePublished 108 _ // ... 109 ticketSpent 110 ) 111 112 func parseTicket(ticket *wire.MsgTx, params *chaincfg.Params) ( 113 votingAddr, commitmentAddr stdaddr.StakeAddress, err error) { 114 fail := func(err error) (_, _ stdaddr.StakeAddress, _ error) { 115 return nil, nil, err 116 } 117 if !stake.IsSStx(ticket) { 118 return fail(fmt.Errorf("%v is not a ticket", ticket)) 119 } 120 _, addrs := stdscript.ExtractAddrs(ticket.TxOut[0].Version, ticket.TxOut[0].PkScript, params) 121 if len(addrs) != 1 { 122 return fail(fmt.Errorf("cannot parse voting addr")) 123 } 124 switch addr := addrs[0].(type) { 125 case stdaddr.StakeAddress: 126 votingAddr = addr 127 default: 128 return fail(fmt.Errorf("address cannot be used for voting rights: %v", err)) 129 } 130 commitmentAddr, err = stake.AddrFromSStxPkScrCommitment(ticket.TxOut[1].PkScript, params) 131 if err != nil { 132 return fail(fmt.Errorf("cannot parse commitment address: %w", err)) 133 } 134 return 135 } 136 137 // calcHeights checks if the ticket has been mined, and if so, sets the live 138 // height and expiry height fields. Should be called with mutex already held. 139 func (fp *feePayment) calcHeights() { 140 _, minedHeight, err := fp.client.wallet.TxBlock(fp.ctx, &fp.ticketHash) 141 if err != nil { 142 // This is not expected to ever error, as the ticket has already been 143 // fetched from the wallet at least one before this point is reached. 144 log.Errorf("Failed to query block which mines ticket: %v", err) 145 return 146 } 147 148 if minedHeight < 2 { 149 return 150 } 151 152 params := fp.client.wallet.ChainParams() 153 154 // Note the off-by-one; this is correct. Tickets become live one block after 155 // the params would indicate. 156 fp.ticketLive = minedHeight + int32(params.TicketMaturity) + 1 157 fp.ticketExpires = fp.ticketLive + int32(params.TicketExpiry) 158 } 159 160 // expiryHeight returns the height at which the ticket expires. Returns zero if 161 // the block is not yet mined. Should be called with mutex already held. 162 func (fp *feePayment) expiryHeight() int32 { 163 if fp.ticketExpires == 0 { 164 fp.calcHeights() 165 } 166 167 return fp.ticketExpires 168 } 169 170 // liveHeight returns the height at which the ticket becomes live. Returns zero 171 // if the block is not yet mined. Should be called with mutex already held. 172 func (fp *feePayment) liveHeight() int32 { 173 if fp.ticketLive == 0 { 174 fp.calcHeights() 175 } 176 177 return fp.ticketLive 178 } 179 180 func (fp *feePayment) ticketSpent() bool { 181 ctx := fp.ctx 182 ticketOut := wire.OutPoint{Hash: fp.ticketHash, Index: 0, Tree: 1} 183 _, _, err := fp.client.wallet.Spender(ctx, &ticketOut) 184 return err == nil 185 } 186 187 func (fp *feePayment) ticketExpired() bool { 188 ctx := fp.ctx 189 w := fp.client.wallet 190 _, tipHeight := w.MainChainTip(ctx) 191 192 fp.mu.Lock() 193 expires := fp.expiryHeight() 194 fp.mu.Unlock() 195 196 return expires > 0 && tipHeight >= expires 197 } 198 199 func (fp *feePayment) removedExpiredOrSpent() bool { 200 var reason string 201 switch { 202 case fp.ticketExpired(): 203 reason = "expired" 204 case fp.ticketSpent(): 205 reason = "spent" 206 } 207 if reason != "" { 208 fp.remove(reason) 209 // nothing scheduled 210 return true 211 } 212 return false 213 } 214 215 func (fp *feePayment) remove(reason string) { 216 fp.stop() 217 log.Infof("ticket %v is %s; removing from VSP client", &fp.ticketHash, reason) 218 fp.client.mu.Lock() 219 delete(fp.client.jobs, fp.ticketHash) 220 fp.client.mu.Unlock() 221 } 222 223 // feePayment returns an existing managed fee payment, or creates and begins 224 // processing a fee payment for a ticket. 225 func (c *Client) feePayment(ctx context.Context, ticketHash *chainhash.Hash, paidConfirmed bool) (fp *feePayment) { 226 c.mu.Lock() 227 fp = c.jobs[*ticketHash] 228 c.mu.Unlock() 229 if fp != nil { 230 return fp 231 } 232 233 defer func() { 234 if fp == nil { 235 return 236 } 237 var schedule bool 238 c.mu.Lock() 239 fp2 := c.jobs[*ticketHash] 240 if fp2 != nil { 241 fp.stop() 242 fp = fp2 243 } else { 244 c.jobs[*ticketHash] = fp 245 schedule = true 246 } 247 c.mu.Unlock() 248 if schedule { 249 fp.schedule("reconcile payment", fp.reconcilePayment) 250 } 251 }() 252 253 w := c.wallet 254 params := w.ChainParams() 255 256 fp = &feePayment{ 257 client: c, 258 ctx: context.Background(), 259 ticketHash: *ticketHash, 260 policy: c.policy, 261 } 262 263 // No VSP interaction is required for spent tickets. 264 if fp.ticketSpent() { 265 fp.state = ticketSpent 266 return fp 267 } 268 269 ticket, err := c.tx(ctx, ticketHash) 270 if err != nil { 271 log.Warnf("no ticket found for %v", ticketHash) 272 return nil 273 } 274 275 fp.votingAddr, fp.commitmentAddr, err = parseTicket(ticket, params) 276 if err != nil { 277 log.Errorf("%v is not a ticket: %v", ticketHash, err) 278 return nil 279 } 280 // Try to access the voting key. 281 fp.votingKey, err = w.DumpWIFPrivateKey(ctx, fp.votingAddr) 282 if err != nil { 283 log.Errorf("no voting key for ticket %v: %v", ticketHash, err) 284 return nil 285 } 286 feeHash, err := w.VSPFeeHashForTicket(ctx, ticketHash) 287 if err != nil { 288 // caller must schedule next method, as paying the fee may 289 // require using provided transaction inputs. 290 return fp 291 } 292 293 fee, err := c.tx(ctx, &feeHash) 294 if err != nil { 295 // A fee hash is recorded for this ticket, but was not found in 296 // the wallet. This should not happen and may require manual 297 // intervention. 298 // 299 // XXX should check ticketinfo and see if fee is not paid. if 300 // possible, update it with a new fee. 301 fp.err = fmt.Errorf("fee transaction not found in wallet: %w", err) 302 return fp 303 } 304 305 fp.feeTx = fee 306 fp.feeHash = feeHash 307 308 // If database has been updated to paid or confirmed status, we can forgo 309 // this step. 310 if !paidConfirmed { 311 err = w.UpdateVspTicketFeeToStarted(ctx, ticketHash, &feeHash, c.Client.URL, c.Client.PubKey) 312 if err != nil { 313 return fp 314 } 315 316 fp.state = unprocessed // XXX fee created, but perhaps not submitted with vsp. 317 fp.fee = -1 // XXX fee amount (not needed anymore?) 318 } 319 return fp 320 } 321 322 func (c *Client) tx(ctx context.Context, hash *chainhash.Hash) (*wire.MsgTx, error) { 323 txs, _, err := c.wallet.GetTransactionsByHashes(ctx, []*chainhash.Hash{hash}) 324 if err != nil { 325 return nil, err 326 } 327 return txs[0], nil 328 } 329 330 // Schedule a method to be executed. 331 // Any currently-scheduled method is replaced. 332 func (fp *feePayment) schedule(name string, method func() error) { 333 var delay time.Duration 334 if method != nil { 335 delay = fp.next() 336 } 337 338 fp.timerMu.Lock() 339 defer fp.timerMu.Unlock() 340 if fp.timer != nil { 341 fp.timer.Stop() 342 fp.timer = nil 343 } 344 if method != nil { 345 log.Debugf("scheduling %q for ticket %s in %v", name, &fp.ticketHash, delay) 346 fp.timer = time.AfterFunc(delay, fp.task(name, method)) 347 } 348 } 349 350 func (fp *feePayment) next() time.Duration { 351 w := fp.client.wallet 352 params := w.ChainParams() 353 _, tipHeight := w.MainChainTip(fp.ctx) 354 355 fp.mu.Lock() 356 ticketLive := fp.liveHeight() 357 ticketExpires := fp.expiryHeight() 358 fp.mu.Unlock() 359 360 var jitter time.Duration 361 switch { 362 case tipHeight < ticketLive: // immature, mined ticket 363 blocksUntilLive := ticketLive - tipHeight 364 jitter = params.TargetTimePerBlock * time.Duration(blocksUntilLive) 365 if jitter > immatureJitter { 366 jitter = immatureJitter 367 } 368 case tipHeight < ticketExpires: // live ticket 369 jitter = liveJitter 370 default: // unmined ticket 371 jitter = unminedJitter 372 } 373 374 return prng.duration(jitter) 375 } 376 377 // task returns a function running a feePayment method. 378 // If the method errors, the error is logged, and the payment is put 379 // in an errored state and may require manual processing. 380 func (fp *feePayment) task(name string, method func() error) func() { 381 return func() { 382 err := method() 383 fp.mu.Lock() 384 fp.err = err 385 fp.mu.Unlock() 386 if err != nil { 387 log.Errorf("ticket %v: %v: %v", &fp.ticketHash, name, err) 388 } 389 } 390 } 391 392 func (fp *feePayment) stop() { 393 fp.schedule("", nil) 394 } 395 396 func (fp *feePayment) receiveFeeAddress() error { 397 ctx := fp.ctx 398 w := fp.client.wallet 399 params := w.ChainParams() 400 401 // stop processing if ticket is expired or spent 402 if fp.removedExpiredOrSpent() { 403 // nothing scheduled 404 return errStopped 405 } 406 407 // Fetch ticket and its parent transaction (typically, a split 408 // transaction). 409 ticket, err := fp.client.tx(ctx, &fp.ticketHash) 410 if err != nil { 411 return fmt.Errorf("failed to retrieve ticket: %w", err) 412 } 413 parentHash := &ticket.TxIn[0].PreviousOutPoint.Hash 414 parent, err := fp.client.tx(ctx, parentHash) 415 if err != nil { 416 return fmt.Errorf("failed to retrieve parent %v of ticket: %w", 417 parentHash, err) 418 } 419 420 ticketHex, err := marshalTx(ticket) 421 if err != nil { 422 return err 423 } 424 parentHex, err := marshalTx(parent) 425 if err != nil { 426 return err 427 } 428 429 req := types.FeeAddressRequest{ 430 Timestamp: time.Now().Unix(), 431 TicketHash: fp.ticketHash.String(), 432 TicketHex: ticketHex, 433 ParentHex: parentHex, 434 } 435 436 resp, err := fp.client.FeeAddress(ctx, req, fp.commitmentAddr) 437 if err != nil { 438 return err 439 } 440 441 feeAmount := dcrutil.Amount(resp.FeeAmount) 442 feeAddr, err := stdaddr.DecodeAddress(resp.FeeAddress, params) 443 if err != nil { 444 return fmt.Errorf("server fee address invalid: %w", err) 445 } 446 447 log.Infof("VSP requires fee %v", feeAmount) 448 if feeAmount > fp.policy.MaxFee { 449 return fmt.Errorf("server fee amount too high: %v > %v", 450 feeAmount, fp.policy.MaxFee) 451 } 452 453 // XXX validate server timestamp? 454 455 fp.mu.Lock() 456 fp.fee = feeAmount 457 fp.feeAddr = feeAddr 458 fp.mu.Unlock() 459 460 return nil 461 } 462 463 // makeFeeTx adds outputs to tx to pay a VSP fee, optionally adding inputs as 464 // well to fund the transaction if no input value is already provided in the 465 // transaction. 466 // 467 // If tx is nil, fp.feeTx may be assigned or modified, but the pointer will not 468 // be dereferenced. 469 func (fp *feePayment) makeFeeTx(tx *wire.MsgTx) error { 470 ctx := fp.ctx 471 w := fp.client.wallet 472 473 fp.mu.Lock() 474 fee := fp.fee 475 fpFeeTx := fp.feeTx 476 feeAddr := fp.feeAddr 477 fp.mu.Unlock() 478 479 // The rest of this function will operate on the tx pointer, with fp.feeTx 480 // assigned to the result on success. 481 // Update tx to use the partially created fpFeeTx if any has been started. 482 // The transaction pointed to by the caller will be dereferenced and modified 483 // when non-nil. 484 if fpFeeTx != nil { 485 if tx != nil { 486 *tx = *fpFeeTx 487 } else { 488 tx = fpFeeTx 489 } 490 } 491 // Fee transaction with outputs is already finished. 492 if fpFeeTx != nil && len(fpFeeTx.TxOut) != 0 { 493 return nil 494 } 495 // When both transactions are nil, create a new empty transaction. 496 if tx == nil { 497 tx = wire.NewMsgTx() 498 } 499 500 // XXX fp.fee == -1? 501 if fee == 0 { 502 err := fp.receiveFeeAddress() 503 if err != nil { 504 return err 505 } 506 fp.mu.Lock() 507 fee = fp.fee 508 feeAddr = fp.feeAddr 509 fp.mu.Unlock() 510 } 511 512 // Reserve new outputs to pay the fee if outputs have not already been 513 // reserved. This will be the case for fee payments that were begun on 514 // already purchased tickets, where the caller did not ensure that fee 515 // outputs would already be reserved. 516 if len(tx.TxIn) == 0 { 517 const minconf = 1 518 inputs, err := w.ReserveOutputsForAmount(ctx, fp.policy.FeeAcct, fee, minconf) 519 if err != nil { 520 return fmt.Errorf("unable to reserve enough output value to "+ 521 "pay VSP fee for ticket %v: %w", fp.ticketHash, err) 522 } 523 for _, in := range inputs { 524 tx.AddTxIn(wire.NewTxIn(&in.OutPoint, in.PrevOut.Value, nil)) 525 } 526 // The transaction will be added to the wallet in an unpublished 527 // state, so there is no need to leave the outputs locked. 528 defer func() { 529 for _, in := range inputs { 530 w.UnlockOutpoint(&in.OutPoint.Hash, in.OutPoint.Index) 531 } 532 }() 533 } 534 535 var input int64 536 for _, in := range tx.TxIn { 537 input += in.ValueIn 538 } 539 if input < int64(fee) { 540 err := fmt.Errorf("not enough input value to pay fee: %v < %v", 541 dcrutil.Amount(input), fee) 542 return err 543 } 544 545 vers, feeScript := feeAddr.PaymentScript() 546 547 addr, err := w.NewChangeAddress(ctx, fp.policy.ChangeAcct) 548 if err != nil { 549 log.Warnf("failed to get new change address: %v", err) 550 return err 551 } 552 var changeOut *wire.TxOut 553 switch addr := addr.(type) { 554 case wallet.Address: 555 vers, script := addr.PaymentScript() 556 changeOut = &wire.TxOut{PkScript: script, Version: vers} 557 default: 558 return fmt.Errorf("failed to convert '%T' to wallet.Address", addr) 559 } 560 561 tx.TxOut = append(tx.TxOut[:0], &wire.TxOut{ 562 Value: int64(fee), 563 Version: vers, 564 PkScript: feeScript, 565 }) 566 feeRate := w.RelayFee() 567 scriptSizes := make([]int, len(tx.TxIn)) 568 for i := range scriptSizes { 569 scriptSizes[i] = txsizes.RedeemP2PKHSigScriptSize 570 } 571 est := txsizes.EstimateSerializeSize(scriptSizes, tx.TxOut, txsizes.P2PKHPkScriptSize) 572 change := input 573 change -= tx.TxOut[0].Value 574 change -= int64(txrules.FeeForSerializeSize(feeRate, est)) 575 if !txrules.IsDustAmount(dcrutil.Amount(change), txsizes.P2PKHPkScriptSize, feeRate) { 576 changeOut.Value = change 577 tx.TxOut = append(tx.TxOut, changeOut) 578 // randomize position 579 if prng.coinflip() { 580 tx.TxOut[0], tx.TxOut[1] = tx.TxOut[1], tx.TxOut[0] 581 } 582 } 583 584 feeHash := tx.TxHash() 585 586 // sign 587 sigErrs, err := w.SignTransaction(ctx, tx, txscript.SigHashAll, nil, nil, nil) 588 if err != nil || len(sigErrs) > 0 { 589 log.Errorf("failed to sign transaction: %v", err) 590 sigErrStr := "" 591 for _, sigErr := range sigErrs { 592 log.Errorf("\t%v", sigErr) 593 sigErrStr = fmt.Sprintf("\t%v", sigErr) + " " 594 } 595 if err != nil { 596 return err 597 } 598 return fmt.Errorf(sigErrStr) 599 } 600 601 err = w.SetPublished(ctx, &feeHash, false) 602 if err != nil { 603 return err 604 } 605 err = w.AddTransaction(ctx, tx, nil) 606 if err != nil { 607 return err 608 } 609 err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey) 610 if err != nil { 611 return err 612 } 613 614 fp.mu.Lock() 615 fp.feeTx = tx 616 fp.feeHash = feeHash 617 fp.mu.Unlock() 618 619 // nothing scheduled 620 return nil 621 } 622 623 func (c *Client) status(ctx context.Context, ticketHash *chainhash.Hash) (*types.TicketStatusResponse, error) { 624 w := c.wallet 625 params := w.ChainParams() 626 627 ticketTx, err := c.tx(ctx, ticketHash) 628 if err != nil { 629 return nil, fmt.Errorf("failed to retrieve ticket %v: %w", ticketHash, err) 630 } 631 if len(ticketTx.TxOut) != 3 { 632 return nil, fmt.Errorf("ticket %v has multiple commitments: %w", ticketHash, errNotSolo) 633 } 634 635 if !stake.IsSStx(ticketTx) { 636 return nil, fmt.Errorf("%v is not a ticket", ticketHash) 637 } 638 commitmentAddr, err := stake.AddrFromSStxPkScrCommitment(ticketTx.TxOut[1].PkScript, params) 639 if err != nil { 640 return nil, fmt.Errorf("failed to extract commitment address from %v: %w", 641 ticketHash, err) 642 } 643 644 req := types.TicketStatusRequest{ 645 TicketHash: ticketHash.String(), 646 } 647 648 resp, err := c.Client.TicketStatus(ctx, req, commitmentAddr) 649 if err != nil { 650 return nil, err 651 } 652 653 // XXX validate server timestamp? 654 655 return resp, nil 656 } 657 658 func (c *Client) setVoteChoices(ctx context.Context, ticketHash *chainhash.Hash, 659 choices map[string]string, tspendPolicy map[string]string, treasuryPolicy map[string]string) error { 660 w := c.wallet 661 params := w.ChainParams() 662 663 ticketTx, err := c.tx(ctx, ticketHash) 664 if err != nil { 665 return fmt.Errorf("failed to retrieve ticket %v: %w", ticketHash, err) 666 } 667 668 if !stake.IsSStx(ticketTx) { 669 return fmt.Errorf("%v is not a ticket", ticketHash) 670 } 671 if len(ticketTx.TxOut) != 3 { 672 return fmt.Errorf("ticket %v has multiple commitments: %w", ticketHash, errNotSolo) 673 } 674 675 commitmentAddr, err := stake.AddrFromSStxPkScrCommitment(ticketTx.TxOut[1].PkScript, params) 676 if err != nil { 677 return fmt.Errorf("failed to extract commitment address from %v: %w", 678 ticketHash, err) 679 } 680 681 req := types.SetVoteChoicesRequest{ 682 Timestamp: time.Now().Unix(), 683 TicketHash: ticketHash.String(), 684 VoteChoices: choices, 685 TSpendPolicy: tspendPolicy, 686 TreasuryPolicy: treasuryPolicy, 687 } 688 689 _, err = c.Client.SetVoteChoices(ctx, req, commitmentAddr) 690 if err != nil { 691 return err 692 } 693 694 // XXX validate server timestamp? 695 696 return nil 697 } 698 699 func (fp *feePayment) reconcilePayment() error { 700 ctx := fp.ctx 701 w := fp.client.wallet 702 703 // stop processing if ticket is expired or spent 704 // XXX if ticket is no longer saved by wallet (because the tx expired, 705 // or was double spent, etc) remove the fee payment. 706 if fp.removedExpiredOrSpent() { 707 // nothing scheduled 708 return errStopped 709 } 710 711 // A fee amount and address must have been created by this point. 712 // Ensure that the fee transaction can be created, otherwise reschedule 713 // this method until it is. There is no need to check the wallet for a 714 // fee transaction matching a known hash; this is performed when 715 // creating the feePayment. 716 fp.mu.Lock() 717 feeTx := fp.feeTx 718 fp.mu.Unlock() 719 if feeTx == nil || len(feeTx.TxOut) == 0 { 720 err := fp.makeFeeTx(nil) 721 if err != nil { 722 var apiErr types.ErrorResponse 723 if errors.As(err, &apiErr) && apiErr.Code == types.ErrTicketCannotVote { 724 fp.remove("ticket cannot vote") 725 } 726 return err 727 } 728 } 729 730 // A fee address has been obtained, and the fee transaction has been 731 // created, but it is unknown if the VSP has received the fee and will 732 // vote using the ticket. 733 // 734 // If the fee is mined, then check the status of the ticket and payment 735 // with the VSP, to ensure that it has marked the fee payment as paid. 736 // 737 // If the fee is not mined, an API call with the VSP is used so it may 738 // receive and publish the transaction. A follow up on the ticket 739 // status is scheduled for some time in the future. 740 741 err := fp.submitPayment() 742 fp.mu.Lock() 743 feeHash := fp.feeHash 744 fp.mu.Unlock() 745 var apiErr types.ErrorResponse 746 if errors.As(err, &apiErr) { 747 switch apiErr.Code { 748 case types.ErrFeeAlreadyReceived: 749 err = w.SetPublished(ctx, &feeHash, true) 750 if err != nil { 751 return err 752 } 753 err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey) 754 if err != nil { 755 return err 756 } 757 err = nil 758 case types.ErrInvalidFeeTx, types.ErrCannotBroadcastFee: 759 err := w.UpdateVspTicketFeeToErrored(ctx, &fp.ticketHash, fp.client.URL, fp.client.PubKey) 760 if err != nil { 761 return err 762 } 763 // Attempt to create a new fee transaction 764 fp.mu.Lock() 765 fp.feeHash = chainhash.Hash{} 766 fp.feeTx = nil 767 fp.mu.Unlock() 768 // err not nilled, so reconcile payment is rescheduled. 769 } 770 } 771 if err != nil { 772 // Nothing left to try except trying again. 773 fp.schedule("reconcile payment", fp.reconcilePayment) 774 return err 775 } 776 777 err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey) 778 if err != nil { 779 return err 780 } 781 782 // confirmPayment will remove the fee payment processing when the fee 783 // has reached sufficient confirmations, and reschedule itself if the 784 // fee is not confirmed yet. If the fee tx is ever removed from the 785 // wallet, this will schedule another reconcile. 786 return fp.confirmPayment() 787 788 /* 789 // XXX? for each input, c.Wallet.UnlockOutpoint(&outpoint.Hash, outpoint.Index) 790 // xxx, or let the published tx replace the unpublished one, and unlock 791 // outpoints as it is processed. 792 793 */ 794 } 795 796 func (fp *feePayment) submitPayment() (err error) { 797 ctx := fp.ctx 798 w := fp.client.wallet 799 800 // stop processing if ticket is expired or spent 801 if fp.removedExpiredOrSpent() { 802 // nothing scheduled 803 return errStopped 804 } 805 806 // submitting a payment requires the fee tx to already be created. 807 fp.mu.Lock() 808 feeTx := fp.feeTx 809 votingKey := fp.votingKey 810 fp.mu.Unlock() 811 if feeTx == nil { 812 feeTx = new(wire.MsgTx) 813 } 814 if len(feeTx.TxOut) == 0 { 815 err := fp.makeFeeTx(feeTx) 816 if err != nil { 817 return err 818 } 819 } 820 if votingKey == "" { 821 votingKey, err = w.DumpWIFPrivateKey(ctx, fp.votingAddr) 822 if err != nil { 823 return err 824 } 825 fp.mu.Lock() 826 fp.votingKey = votingKey 827 fp.mu.Unlock() 828 } 829 830 // Retrieve voting preferences 831 voteChoices := make(map[string]string) 832 agendaChoices, _, err := w.AgendaChoices(ctx, &fp.ticketHash) 833 if err != nil { 834 return err 835 } 836 for _, agendaChoice := range agendaChoices { 837 voteChoices[agendaChoice.AgendaID] = agendaChoice.ChoiceID 838 } 839 840 feeTxHex, err := marshalTx(feeTx) 841 if err != nil { 842 return err 843 } 844 845 req := types.PayFeeRequest{ 846 Timestamp: time.Now().Unix(), 847 TicketHash: fp.ticketHash.String(), 848 FeeTx: feeTxHex, 849 VotingKey: votingKey, 850 VoteChoices: voteChoices, 851 TSpendPolicy: w.TSpendPolicyForTicket(&fp.ticketHash), 852 TreasuryPolicy: w.TreasuryKeyPolicyForTicket(&fp.ticketHash), 853 } 854 855 _, err = fp.client.PayFee(ctx, req, fp.commitmentAddr) 856 if err != nil { 857 var apiErr types.ErrorResponse 858 if errors.As(err, &apiErr) && apiErr.Code == types.ErrFeeExpired { 859 // Fee has been expired, so abandon current feetx, set fp.feeTx 860 // to nil and retry submit payment to make a new fee tx. 861 feeHash := feeTx.TxHash() 862 err := w.AbandonTransaction(ctx, &feeHash) 863 if err != nil { 864 log.Errorf("error abandoning expired fee tx %v", err) 865 } 866 fp.mu.Lock() 867 fp.feeTx = nil 868 fp.mu.Unlock() 869 } 870 return fmt.Errorf("payfee: %w", err) 871 } 872 873 // TODO - validate server timestamp? 874 875 log.Infof("successfully processed %v", fp.ticketHash) 876 return nil 877 } 878 879 func (fp *feePayment) confirmPayment() (err error) { 880 ctx := fp.ctx 881 w := fp.client.wallet 882 883 // stop processing if ticket is expired or spent 884 if fp.removedExpiredOrSpent() { 885 // nothing scheduled 886 return errStopped 887 } 888 889 defer func() { 890 if err != nil && !errors.Is(err, errStopped) { 891 fp.schedule("reconcile payment", fp.reconcilePayment) 892 } 893 }() 894 895 status, err := fp.client.status(ctx, &fp.ticketHash) 896 if err != nil { 897 log.Warnf("Rescheduling status check for %v: %v", &fp.ticketHash, err) 898 fp.schedule("confirm payment", fp.confirmPayment) 899 return nil 900 } 901 902 switch status.FeeTxStatus { 903 case "received": 904 // VSP has received the fee tx but has not yet broadcast it. 905 // VSP will only broadcast the tx when ticket has 6+ confirmations. 906 fp.schedule("confirm payment", fp.confirmPayment) 907 return nil 908 case "broadcast": 909 log.Infof("VSP has successfully sent the fee tx for %v", &fp.ticketHash) 910 // Broadcasted, but not confirmed. 911 fp.schedule("confirm payment", fp.confirmPayment) 912 return nil 913 case "confirmed": 914 fp.remove("confirmed by VSP") 915 // nothing scheduled 916 fp.mu.Lock() 917 feeHash := fp.feeHash 918 fp.mu.Unlock() 919 err = w.UpdateVspTicketFeeToConfirmed(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey) 920 if err != nil { 921 return err 922 } 923 return nil 924 case "error": 925 log.Warnf("VSP failed to broadcast feetx for %v -- restarting payment", 926 &fp.ticketHash) 927 fp.schedule("reconcile payment", fp.reconcilePayment) 928 return nil 929 default: 930 // XXX put in unknown state 931 log.Warnf("VSP responded with %v for %v", status.FeeTxStatus, 932 &fp.ticketHash) 933 } 934 935 return nil 936 } 937 938 func marshalTx(tx *wire.MsgTx) (string, error) { 939 var buf bytes.Buffer 940 buf.Grow(tx.SerializeSize() * 2) 941 err := tx.Serialize(hex.NewEncoder(&buf)) 942 return buf.String(), err 943 }