client: Remove autoclient & bump to v3

Adding the autoclient to vspd created a circular dependency between vspd
and dcrwallet which is cumbersome and not worth the maintence burden.

At this point there are no known consumers of the vspd version of
autoclient, so removing it is not expected to cause problems for
anybody.

The autoclient will continue to live in the dcrwallet repo.
This commit is contained in:
jholdstock 2023-08-09 09:49:29 +01:00 committed by Jamie Holdstock
parent 45addd2a18
commit 2db761e072
5 changed files with 3 additions and 1459 deletions

View File

@ -27,18 +27,3 @@ linters:
- unparam
- unused
- vetshadow
linters-settings:
# Disable rule SA1019 on staticcheck, it causes the build to fail if any
# deprecated func/var/const are referenced.
staticcheck:
checks: ["all", "-SA1019"]
exhaustive:
check:
- switch
- map
# Presence of "default" case in switch statements satisfies exhaustiveness,
# even if all enum members are not listed.
# Default: false
default-signifies-exhaustive: true

View File

@ -1,447 +0,0 @@
// Copyright (c) 2022-2023 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package client
import (
"context"
"encoding/base64"
"fmt"
"net"
"net/http"
"net/url"
"sync"
"decred.org/dcrwallet/v3/errors"
"decred.org/dcrwallet/v3/wallet"
"decred.org/dcrwallet/v3/wallet/udb"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/dcrutil/v4"
"github.com/decred/dcrd/txscript/v4"
"github.com/decred/dcrd/txscript/v4/stdaddr"
"github.com/decred/dcrd/wire"
"github.com/decred/slog"
)
type DialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
type Policy struct {
MaxFee dcrutil.Amount
ChangeAcct uint32 // to derive fee addresses
FeeAcct uint32 // to pay fees from, if inputs are not provided to Process
}
// Ensure dcrwallet satisfies the Wallet interface.
var _ Wallet = (*wallet.Wallet)(nil)
type Wallet interface {
Spender(ctx context.Context, out *wire.OutPoint) (*wire.MsgTx, uint32, error)
MainChainTip(ctx context.Context) (hash chainhash.Hash, height int32)
ChainParams() *chaincfg.Params
TxBlock(ctx context.Context, hash *chainhash.Hash) (chainhash.Hash, int32, error)
DumpWIFPrivateKey(ctx context.Context, addr stdaddr.Address) (string, error)
VSPFeeHashForTicket(ctx context.Context, ticketHash *chainhash.Hash) (chainhash.Hash, error)
UpdateVspTicketFeeToStarted(ctx context.Context, ticketHash, feeHash *chainhash.Hash, host string, pubkey []byte) error
GetTransactionsByHashes(ctx context.Context, txHashes []*chainhash.Hash) (txs []*wire.MsgTx, notFound []*wire.InvVect, err error)
ReserveOutputsForAmount(ctx context.Context, account uint32, amount dcrutil.Amount, minconf int32) ([]wallet.Input, error)
UnlockOutpoint(txHash *chainhash.Hash, index uint32)
NewChangeAddress(ctx context.Context, account uint32) (stdaddr.Address, error)
RelayFee() dcrutil.Amount
SignTransaction(ctx context.Context, tx *wire.MsgTx, hashType txscript.SigHashType, additionalPrevScripts map[wire.OutPoint][]byte,
additionalKeysByAddress map[string]*dcrutil.WIF, p2shRedeemScriptsByAddress map[string][]byte) ([]wallet.SignatureError, error)
SetPublished(ctx context.Context, hash *chainhash.Hash, published bool) error
AddTransaction(ctx context.Context, tx *wire.MsgTx, blockHash *chainhash.Hash) error
UpdateVspTicketFeeToPaid(ctx context.Context, ticketHash, feeHash *chainhash.Hash, host string, pubkey []byte) error
UpdateVspTicketFeeToErrored(ctx context.Context, ticketHash *chainhash.Hash, host string, pubkey []byte) error
AgendaChoices(ctx context.Context, ticketHash *chainhash.Hash) (choices wallet.AgendaChoices, voteBits uint16, err error)
TSpendPolicyForTicket(ticketHash *chainhash.Hash) map[string]string
TreasuryKeyPolicyForTicket(ticketHash *chainhash.Hash) map[string]string
AbandonTransaction(ctx context.Context, hash *chainhash.Hash) error
TxConfirms(ctx context.Context, hash *chainhash.Hash) (int32, error)
ForUnspentUnexpiredTickets(ctx context.Context, f func(hash *chainhash.Hash) error) error
IsVSPTicketConfirmed(ctx context.Context, ticketHash *chainhash.Hash) (bool, error)
UpdateVspTicketFeeToConfirmed(ctx context.Context, ticketHash, feeHash *chainhash.Hash, host string, pubkey []byte) error
VSPTicketInfo(ctx context.Context, ticketHash *chainhash.Hash) (*wallet.VSPTicket, error)
SignMessage(ctx context.Context, msg string, addr stdaddr.Address) (sig []byte, err error)
}
type AutoClient struct {
wallet Wallet
policy *Policy
*Client
mu sync.Mutex
jobs map[chainhash.Hash]*feePayment
log slog.Logger
}
type Config struct {
// URL specifies the base URL of the VSP
URL string
// PubKey specifies the VSP's base64 encoded public key
PubKey string
// Dialer specifies an optional dialer when connecting to the VSP.
Dialer DialFunc
// Wallet specifies a loaded wallet.
Wallet Wallet
// Default policy for fee payments unless another is provided by the
// caller.
Policy *Policy
}
func New(cfg Config, log slog.Logger) (*AutoClient, error) {
u, err := url.Parse(cfg.URL)
if err != nil {
return nil, err
}
pubKey, err := base64.StdEncoding.DecodeString(cfg.PubKey)
if err != nil {
return nil, err
}
if cfg.Wallet == nil {
return nil, fmt.Errorf("wallet option not set")
}
client := &Client{
URL: u.String(),
PubKey: pubKey,
Sign: cfg.Wallet.SignMessage,
Log: log,
}
client.Transport = &http.Transport{
DialContext: cfg.Dialer,
}
v := &AutoClient{
wallet: cfg.Wallet,
policy: cfg.Policy,
Client: client,
jobs: make(map[chainhash.Hash]*feePayment),
log: log,
}
return v, nil
}
func (c *AutoClient) FeePercentage(ctx context.Context) (float64, error) {
resp, err := c.Client.VspInfo(ctx)
if err != nil {
return -1, err
}
return resp.FeePercentage, nil
}
// ProcessUnprocessedTickets processes all tickets that don't currently have
// any association with a VSP.
func (c *AutoClient) ProcessUnprocessedTickets(ctx context.Context) {
var wg sync.WaitGroup
_ = c.wallet.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.
_, err := c.wallet.VSPFeeHashForTicket(ctx, hash)
if err == nil {
return nil
}
confirmed, err := c.wallet.IsVSPTicketConfirmed(ctx, hash)
if err != nil && !errors.Is(err, errors.NotExist) {
c.log.Error(err)
return nil
}
if confirmed {
return nil
}
c.mu.Lock()
fp := c.jobs[*hash]
c.mu.Unlock()
if fp != nil {
// Already processing this ticket with the VSP.
return nil
}
// Start processing in the background.
wg.Add(1)
go func() {
defer wg.Done()
err := c.Process(ctx, hash, nil)
if err != nil {
c.log.Error(err)
}
}()
return nil
})
wg.Wait()
}
// ProcessTicket attempts to process a given ticket based on the hash provided.
func (c *AutoClient) ProcessTicket(ctx context.Context, hash *chainhash.Hash) error {
err := c.Process(ctx, hash, nil)
if err != nil {
return err
}
return nil
}
// ProcessManagedTickets discovers tickets which were previously registered with
// a VSP and begins syncing them in the background. This is used to recover VSP
// tracking after seed restores, and is only performed on unspent and unexpired
// tickets.
func (c *AutoClient) ProcessManagedTickets(ctx context.Context) error {
err := c.wallet.ForUnspentUnexpiredTickets(ctx, func(hash *chainhash.Hash) error {
// We only want to process tickets that haven't been confirmed yet.
confirmed, err := c.wallet.IsVSPTicketConfirmed(ctx, hash)
if err != nil && !errors.Is(err, errors.NotExist) {
c.log.Error(err)
return nil
}
if confirmed {
return nil
}
c.mu.Lock()
_, ok := c.jobs[*hash]
c.mu.Unlock()
if ok {
// Already processing this ticket with the VSP.
return nil
}
// 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)
if err != nil {
if errors.Is(err, errors.Locked) {
return err
}
return nil
}
if status.FeeTxStatus == "confirmed" {
feeHash, err := chainhash.NewHashFromStr(status.FeeTxHash)
if err != nil {
return err
}
err = c.wallet.UpdateVspTicketFeeToConfirmed(ctx, hash, feeHash, c.Client.URL, c.Client.PubKey)
if err != nil {
return err
}
return nil
} else if status.FeeTxHash != "" {
feeHash, err := chainhash.NewHashFromStr(status.FeeTxHash)
if err != nil {
return err
}
err = c.wallet.UpdateVspTicketFeeToPaid(ctx, hash, feeHash, c.Client.URL, c.Client.PubKey)
if err != nil {
return err
}
_ = c.feePayment(ctx, hash, true)
} else {
// Fee hasn't been paid at the provided VSP, so this should do that if needed.
_ = c.feePayment(ctx, hash, false)
}
return nil
})
return err
}
// Process begins processing a VSP fee payment for a ticket. If feeTx contains
// inputs, is used to pay the VSP fee. Otherwise, new inputs are selected and
// locked to prevent double spending the fee.
//
// feeTx must not be nil, but may point to an empty transaction, and is modified
// with 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 *AutoClient) Process(ctx context.Context, ticketHash *chainhash.Hash, feeTx *wire.MsgTx) error {
vspTicket, err := c.wallet.VSPTicketInfo(ctx, ticketHash)
if err != nil && !errors.Is(err, errors.NotExist) {
return err
}
feeStatus := udb.VSPFeeProcessStarted // Will be used if the ticket isn't registered to the vsp yet.
if vspTicket != nil {
feeStatus = udb.FeeStatus(vspTicket.FeeTxStatus)
}
switch feeStatus {
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)
if fp == nil {
err := c.wallet.UpdateVspTicketFeeToErrored(ctx, ticketHash, c.Client.URL, c.Client.PubKey)
if err != nil {
return err
}
return fmt.Errorf("fee payment cannot be processed")
}
fp.mu.Lock()
if fp.feeTx == nil {
fp.feeTx = feeTx
}
fp.mu.Unlock()
err := fp.receiveFeeAddress()
if err != nil {
err := c.wallet.UpdateVspTicketFeeToErrored(ctx, ticketHash, c.Client.URL, c.Client.PubKey)
if err != nil {
return err
}
// XXX, retry? (old Process retried)
// but this may not be necessary any longer as the parent of
// the ticket is always relayed to the vsp as well.
return err
}
err = fp.makeFeeTx(feeTx)
if err != nil {
err := c.wallet.UpdateVspTicketFeeToErrored(ctx, ticketHash, c.Client.URL, c.Client.PubKey)
if err != nil {
return err
}
return err
}
return fp.submitPayment()
case udb.VSPFeeProcessPaid:
// If a VSP ticket has been paid, but confirm payment.
if len(vspTicket.Host) > 0 && vspTicket.Host != c.Client.URL {
// 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)
if fp == nil {
// Don't update VSPStatus to Errored if it was already paid or
// confirmed.
return fmt.Errorf("fee payment cannot be processed")
}
return fp.confirmPayment()
case udb.VSPFeeProcessConfirmed:
// VSPTicket has already been confirmed, there is nothing to process.
return nil
}
return nil
}
// SetVoteChoice takes the provided consensus, tspend and treasury key voting
// 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 *AutoClient) SetVoteChoice(ctx context.Context, hash *chainhash.Hash,
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)
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)
return nil
}
// Check for any mismatch between the provided voting preferences and the
// VSP preferences to determine if VSP needs to be updated.
update := false
// Check consensus vote choices.
for newAgenda, newChoice := range choices {
vspChoice, ok := status.VoteChoices[newAgenda]
if !ok {
update = true
break
}
if vspChoice != newChoice {
update = true
break
}
}
// Check tspend policies.
for newTSpend, newChoice := range tspendPolicy {
vspChoice, ok := status.TSpendPolicy[newTSpend]
if !ok {
update = true
break
}
if vspChoice != newChoice {
update = true
break
}
}
// Check treasury policies.
for newKey, newChoice := range treasuryPolicy {
vspChoice, ok := status.TSpendPolicy[newKey]
if !ok {
update = true
break
}
if vspChoice != newChoice {
update = true
break
}
}
if !update {
c.log.Debugf("VSP already has correct vote choices for ticket %s", hash)
return nil
}
c.log.Debugf("Updating vote choices on VSP for ticket %s", hash)
err = c.setVoteChoices(ctx, hash, choices, tspendPolicy, treasuryPolicy)
if err != nil {
return err
}
return nil
}
// TicketInfo stores per-ticket info tracked by a VSP Client instance.
type TicketInfo struct {
TicketHash chainhash.Hash
CommitmentAddr stdaddr.StakeAddress
VotingAddr stdaddr.StakeAddress
State State
Fee dcrutil.Amount
FeeHash chainhash.Hash
// TODO: include stuff returned by the status() call?
}
// TrackedTickets returns information about all outstanding tickets tracked by
// a vsp.Client instance.
//
// Currently this returns only info about tickets which fee hasn't been paid or
// confirmed at enough depth to be considered committed to.
func (c *AutoClient) TrackedTickets() []*TicketInfo {
// Collect all jobs first, to avoid working under two different locks.
c.mu.Lock()
jobs := make([]*feePayment, 0, len(c.jobs))
for _, job := range c.jobs {
jobs = append(jobs, job)
}
c.mu.Unlock()
tickets := make([]*TicketInfo, 0, len(jobs))
for _, job := range jobs {
job.mu.Lock()
tickets = append(tickets, &TicketInfo{
TicketHash: job.ticketHash,
CommitmentAddr: job.commitmentAddr,
VotingAddr: job.votingAddr,
State: job.state,
Fee: job.fee,
FeeHash: job.feeHash,
})
job.mu.Unlock()
}
return tickets
}

View File

@ -1,937 +0,0 @@
// Copyright (c) 2022-2023 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package client
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"math/big"
"sync"
"time"
wallet_errs "decred.org/dcrwallet/v3/errors"
"decred.org/dcrwallet/v3/wallet"
"decred.org/dcrwallet/v3/wallet/txrules"
"decred.org/dcrwallet/v3/wallet/txsizes"
"github.com/decred/dcrd/blockchain/stake/v5"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/dcrutil/v4"
"github.com/decred/dcrd/txscript/v4"
"github.com/decred/dcrd/txscript/v4/stdaddr"
"github.com/decred/dcrd/txscript/v4/stdscript"
"github.com/decred/dcrd/wire"
"github.com/decred/slog"
"github.com/decred/vspd/types/v2"
)
// randomInt64 returns a random int64 in [0,n).
func randomInt64(n int64) int64 {
i, err := rand.Int(rand.Reader, big.NewInt(n))
if err != nil {
// crypto/rand should never return an error if running on a supported platform.
panic(fmt.Sprintf("unhandled crypto/rand error: %v", err))
}
return i.Int64()
}
// randomDuration returns a random time.Duration in [0,d).
func randomDuration(d time.Duration) time.Duration {
return time.Duration(randomInt64(int64(d)))
}
// coinflip returns a random bool.
func coinflip() bool {
return randomInt64(2) == 0
}
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
// before performing some background action with the VSP. The delay is reduced
// when a ticket is currently live, as it may be called to vote any time.
const (
immatureJitter = time.Hour
liveJitter = 5 * time.Minute
unminedJitter = 2 * time.Minute
)
type feePayment struct {
client *AutoClient
ctx context.Context
// Set at feepayment creation and never changes
ticketHash chainhash.Hash
commitmentAddr stdaddr.StakeAddress
votingAddr stdaddr.StakeAddress
policy *Policy
// Requires locking for all access outside of Client.feePayment
mu sync.Mutex
votingKey string
ticketLive int32
ticketExpires int32
fee dcrutil.Amount
feeAddr stdaddr.Address
feeHash chainhash.Hash
feeTx *wire.MsgTx
state State
err error
timerMu sync.Mutex
timer *time.Timer
log slog.Logger
}
type State uint32
const (
_ State = iota
Unprocessed
FeePublished
_ // ...
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
}
func (fp *feePayment) ticketSpent() bool {
ctx := fp.ctx
ticketOut := wire.OutPoint{Hash: fp.ticketHash, Index: 0, Tree: 1}
_, _, err := fp.client.wallet.Spender(ctx, &ticketOut)
return err == nil
}
func (fp *feePayment) ticketExpired() bool {
ctx := fp.ctx
w := fp.client.wallet
_, tipHeight := w.MainChainTip(ctx)
fp.mu.Lock()
expires := fp.ticketExpires
fp.mu.Unlock()
return expires > 0 && tipHeight >= expires
}
func (fp *feePayment) removedExpiredOrSpent() bool {
var reason string
switch {
case fp.ticketExpired():
reason = "expired"
case fp.ticketSpent():
reason = "spent"
}
if reason != "" {
fp.remove(reason)
// nothing scheduled
return true
}
return false
}
func (fp *feePayment) remove(reason string) {
fp.stop()
fp.log.Infof("ticket %v is %s; removing from VSP client", &fp.ticketHash, reason)
fp.client.mu.Lock()
delete(fp.client.jobs, fp.ticketHash)
fp.client.mu.Unlock()
}
// feePayment returns an existing managed fee payment, or creates and begins
// processing a fee payment for a ticket.
func (c *AutoClient) feePayment(ctx context.Context, ticketHash *chainhash.Hash, paidConfirmed bool) (fp *feePayment) {
c.mu.Lock()
fp = c.jobs[*ticketHash]
c.mu.Unlock()
if fp != nil {
return fp
}
defer func() {
if fp == nil {
return
}
var schedule bool
c.mu.Lock()
fp2 := c.jobs[*ticketHash]
if fp2 != nil {
fp.stop()
fp = fp2
} else {
c.jobs[*ticketHash] = fp
schedule = true
}
c.mu.Unlock()
if schedule {
fp.schedule("reconcile payment", fp.reconcilePayment)
}
}()
w := c.wallet
params := w.ChainParams()
fp = &feePayment{
client: c,
ctx: ctx,
ticketHash: *ticketHash,
policy: c.policy,
log: c.log,
}
// No VSP interaction is required for spent tickets.
if fp.ticketSpent() {
fp.state = TicketSpent
return fp
}
ticket, err := c.tx(ctx, ticketHash)
if err != nil {
fp.log.Warnf("no ticket found for %v", ticketHash)
return nil
}
_, ticketHeight, err := w.TxBlock(ctx, ticketHash)
if err != nil {
// This is not expected to ever error, as the ticket was fetched
// from the wallet in the above call.
fp.log.Errorf("failed to query block which mines ticket: %v", err)
return nil
}
if ticketHeight >= 2 {
// Note the off-by-one; this is correct. Tickets become live
// one block after the params would indicate.
fp.ticketLive = ticketHeight + int32(params.TicketMaturity) + 1
fp.ticketExpires = fp.ticketLive + int32(params.TicketExpiry)
}
fp.votingAddr, fp.commitmentAddr, err = parseTicket(ticket, params)
if err != nil {
fp.log.Errorf("%v is not a ticket: %v", ticketHash, err)
return nil
}
// Try to access the voting key, ignore error unless the wallet is
// locked.
fp.votingKey, err = w.DumpWIFPrivateKey(ctx, fp.votingAddr)
if err != nil && !errors.Is(err, wallet_errs.Locked) {
fp.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
// require using provided transaction inputs.
return fp
}
fee, err := c.tx(ctx, &feeHash)
if err != nil {
// A fee hash is recorded for this ticket, but was not found in
// the wallet. This should not happen and may require manual
// intervention.
//
// XXX should check ticketinfo and see if fee is not paid. if
// possible, update it with a new fee.
fp.err = fmt.Errorf("fee transaction not found in wallet: %w", err)
return fp
}
fp.feeTx = fee
fp.feeHash = feeHash
// If database has been updated to paid or confirmed status, we can forgo
// this step.
if !paidConfirmed {
err = w.UpdateVspTicketFeeToStarted(ctx, ticketHash, &feeHash, c.Client.URL, c.Client.PubKey)
if err != nil {
return fp
}
fp.state = Unprocessed // XXX fee created, but perhaps not submitted with vsp.
fp.fee = -1 // XXX fee amount (not needed anymore?)
}
return fp
}
func (c *AutoClient) tx(ctx context.Context, hash *chainhash.Hash) (*wire.MsgTx, error) {
txs, _, err := c.wallet.GetTransactionsByHashes(ctx, []*chainhash.Hash{hash})
if err != nil {
return nil, err
}
return txs[0], nil
}
// Schedule a method to be executed.
// Any currently-scheduled method is replaced.
func (fp *feePayment) schedule(name string, method func() error) {
var delay time.Duration
if method != nil {
delay = fp.next()
}
fp.timerMu.Lock()
defer fp.timerMu.Unlock()
if fp.timer != nil {
fp.timer.Stop()
fp.timer = nil
}
if method != nil {
fp.log.Debugf("scheduling %q for ticket %s in %v", name, &fp.ticketHash, delay)
fp.timer = time.AfterFunc(delay, fp.task(name, method))
}
}
func (fp *feePayment) next() time.Duration {
w := fp.client.wallet
params := w.ChainParams()
_, tipHeight := w.MainChainTip(fp.ctx)
fp.mu.Lock()
ticketLive := fp.ticketLive
ticketExpires := fp.ticketExpires
fp.mu.Unlock()
var jitter time.Duration
switch {
case tipHeight < ticketLive: // immature, mined ticket
blocksUntilLive := ticketExpires - tipHeight
jitter = params.TargetTimePerBlock * time.Duration(blocksUntilLive)
if jitter > immatureJitter {
jitter = immatureJitter
}
case tipHeight < ticketExpires: // live ticket
jitter = liveJitter
default: // unmined ticket
jitter = unminedJitter
}
return randomDuration(jitter)
}
// task returns a function running a feePayment method.
// If the method errors, the error is logged, and the payment is put
// in an errored state and may require manual processing.
func (fp *feePayment) task(name string, method func() error) func() {
return func() {
err := method()
fp.mu.Lock()
fp.err = err
fp.mu.Unlock()
if err != nil {
fp.log.Errorf("ticket %v: %v: %v", &fp.ticketHash, name, err)
}
}
}
func (fp *feePayment) stop() {
fp.schedule("", nil)
}
func (fp *feePayment) receiveFeeAddress() error {
ctx := fp.ctx
w := fp.client.wallet
params := w.ChainParams()
// stop processing if ticket is expired or spent
if fp.removedExpiredOrSpent() {
// nothing scheduled
return errStopped
}
// Fetch ticket and its parent transaction (typically, a split
// transaction).
ticket, err := fp.client.tx(ctx, &fp.ticketHash)
if err != nil {
return fmt.Errorf("failed to retrieve ticket: %w", err)
}
parentHash := &ticket.TxIn[0].PreviousOutPoint.Hash
parent, err := fp.client.tx(ctx, parentHash)
if err != nil {
return fmt.Errorf("failed to retrieve parent %v of ticket: %w",
parentHash, err)
}
ticketHex, err := marshalTx(ticket)
if err != nil {
return err
}
parentHex, err := marshalTx(parent)
if err != nil {
return err
}
req := types.FeeAddressRequest{
Timestamp: time.Now().Unix(),
TicketHash: fp.ticketHash.String(),
TicketHex: ticketHex,
ParentHex: parentHex,
}
resp, err := fp.client.FeeAddress(ctx, req, fp.commitmentAddr)
if err != nil {
return err
}
feeAmount := dcrutil.Amount(resp.FeeAmount)
feeAddr, err := stdaddr.DecodeAddress(resp.FeeAddress, params)
if err != nil {
return fmt.Errorf("server fee address invalid: %w", err)
}
fp.log.Infof("VSP requires fee %v", feeAmount)
if feeAmount > fp.policy.MaxFee {
return fmt.Errorf("server fee amount too high: %v > %v",
feeAmount, fp.policy.MaxFee)
}
// XXX validate server timestamp?
fp.mu.Lock()
fp.fee = feeAmount
fp.feeAddr = feeAddr
fp.mu.Unlock()
return nil
}
// makeFeeTx adds outputs to tx to pay a VSP fee, optionally adding inputs as
// well to fund the transaction if no input value is already provided in the
// transaction.
//
// If tx is nil, fp.feeTx may be assigned or modified, but the pointer will not
// be dereferenced.
func (fp *feePayment) makeFeeTx(tx *wire.MsgTx) error {
ctx := fp.ctx
w := fp.client.wallet
fp.mu.Lock()
fee := fp.fee
fpFeeTx := fp.feeTx
feeAddr := fp.feeAddr
fp.mu.Unlock()
// The rest of this function will operate on the tx pointer, with fp.feeTx
// assigned to the result on success.
// Update tx to use the partially created fpFeeTx if any has been started.
// The transaction pointed to by the caller will be dereferenced and modified
// when non-nil.
if fpFeeTx != nil {
if tx != nil {
*tx = *fpFeeTx
} else {
tx = fpFeeTx
}
}
// Fee transaction with outputs is already finished.
if fpFeeTx != nil && len(fpFeeTx.TxOut) != 0 {
return nil
}
// When both transactions are nil, create a new empty transaction.
if tx == nil {
tx = wire.NewMsgTx()
}
// XXX fp.fee == -1?
if fee == 0 {
err := fp.receiveFeeAddress()
if err != nil {
return err
}
fp.mu.Lock()
fee = fp.fee
feeAddr = fp.feeAddr
fp.mu.Unlock()
}
// Reserve new outputs to pay the fee if outputs have not already been
// reserved. This will be the case for fee payments that were begun on
// already purchased tickets, where the caller did not ensure that fee
// outputs would already be reserved.
if len(tx.TxIn) == 0 {
const minconf = 1
inputs, err := w.ReserveOutputsForAmount(ctx, fp.policy.FeeAcct, fee, minconf)
if err != nil {
return fmt.Errorf("unable to reserve enough output value to "+
"pay VSP fee for ticket %v: %w", fp.ticketHash, err)
}
for _, in := range inputs {
tx.AddTxIn(wire.NewTxIn(&in.OutPoint, in.PrevOut.Value, nil))
}
// The transaction will be added to the wallet in an unpublished
// state, so there is no need to leave the outputs locked.
defer func() {
for _, in := range inputs {
w.UnlockOutpoint(&in.OutPoint.Hash, in.OutPoint.Index)
}
}()
}
var input int64
for _, in := range tx.TxIn {
input += in.ValueIn
}
if input < int64(fee) {
err := fmt.Errorf("not enough input value to pay fee: %v < %v",
dcrutil.Amount(input), fee)
return err
}
vers, feeScript := feeAddr.PaymentScript()
addr, err := w.NewChangeAddress(ctx, fp.policy.ChangeAcct)
if err != nil {
fp.log.Warnf("failed to get new change address: %v", err)
return err
}
var changeOut *wire.TxOut
switch addr := addr.(type) {
case wallet.Address:
vers, script := addr.PaymentScript()
changeOut = &wire.TxOut{PkScript: script, Version: vers}
default:
return fmt.Errorf("failed to convert '%T' to wallet.Address", addr)
}
tx.TxOut = append(tx.TxOut[:0], &wire.TxOut{
Value: int64(fee),
Version: vers,
PkScript: feeScript,
})
feeRate := w.RelayFee()
scriptSizes := make([]int, len(tx.TxIn))
for i := range scriptSizes {
scriptSizes[i] = txsizes.RedeemP2PKHSigScriptSize
}
est := txsizes.EstimateSerializeSize(scriptSizes, tx.TxOut, txsizes.P2PKHPkScriptSize)
change := input
change -= tx.TxOut[0].Value
change -= int64(txrules.FeeForSerializeSize(feeRate, est))
if !txrules.IsDustAmount(dcrutil.Amount(change), txsizes.P2PKHPkScriptSize, feeRate) {
changeOut.Value = change
tx.TxOut = append(tx.TxOut, changeOut)
// randomize position
if coinflip() {
tx.TxOut[0], tx.TxOut[1] = tx.TxOut[1], tx.TxOut[0]
}
}
feeHash := tx.TxHash()
// sign
sigErrs, err := w.SignTransaction(ctx, tx, txscript.SigHashAll, nil, nil, nil)
if err != nil || len(sigErrs) > 0 {
fp.log.Errorf("failed to sign transaction: %v", err)
sigErrStr := ""
for _, sigErr := range sigErrs {
fp.log.Errorf("\t%v", sigErr)
sigErrStr = fmt.Sprintf("\t%v", sigErr) + " "
}
if err != nil {
return err
}
return fmt.Errorf(sigErrStr)
}
err = w.SetPublished(ctx, &feeHash, false)
if err != nil {
return err
}
err = w.AddTransaction(ctx, tx, nil)
if err != nil {
return err
}
err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey)
if err != nil {
return err
}
fp.mu.Lock()
fp.feeTx = tx
fp.feeHash = feeHash
fp.mu.Unlock()
// nothing scheduled
return nil
}
func (c *AutoClient) status(ctx context.Context, ticketHash *chainhash.Hash) (*types.TicketStatusResponse, error) {
w := c.wallet
params := w.ChainParams()
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, params)
if err != nil {
return nil, fmt.Errorf("failed to extract commitment address from %v: %w",
ticketHash, err)
}
req := types.TicketStatusRequest{
TicketHash: ticketHash.String(),
}
resp, err := c.Client.TicketStatus(ctx, req, commitmentAddr)
if err != nil {
return nil, err
}
// XXX validate server timestamp?
return resp, nil
}
func (c *AutoClient) setVoteChoices(ctx context.Context, ticketHash *chainhash.Hash,
choices map[string]string, tspendPolicy map[string]string, treasuryPolicy map[string]string) error {
w := c.wallet
params := w.ChainParams()
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, 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(),
VoteChoices: choices,
TSpendPolicy: tspendPolicy,
TreasuryPolicy: treasuryPolicy,
}
_, err = c.Client.SetVoteChoices(ctx, req, commitmentAddr)
if err != nil {
return err
}
// XXX validate server timestamp?
return nil
}
func (fp *feePayment) reconcilePayment() error {
ctx := fp.ctx
w := fp.client.wallet
// stop processing if ticket is expired or spent
// XXX if ticket is no longer saved by wallet (because the tx expired,
// or was double spent, etc) remove the fee payment.
if fp.removedExpiredOrSpent() {
// nothing scheduled
return errStopped
}
// A fee amount and address must have been created by this point.
// Ensure that the fee transaction can be created, otherwise reschedule
// this method until it is. There is no need to check the wallet for a
// fee transaction matching a known hash; this is performed when
// creating the feePayment.
fp.mu.Lock()
feeTx := fp.feeTx
fp.mu.Unlock()
if feeTx == nil || len(feeTx.TxOut) == 0 {
err := fp.makeFeeTx(nil)
if err != nil {
var apiErr types.ErrorResponse
if errors.As(err, &apiErr) && apiErr.Code == types.ErrTicketCannotVote {
fp.remove("ticket cannot vote")
}
return err
}
}
// A fee address has been obtained, and the fee transaction has been
// created, but it is unknown if the VSP has received the fee and will
// vote using the ticket.
//
// If the fee is mined, then check the status of the ticket and payment
// with the VSP, to ensure that it has marked the fee payment as paid.
//
// If the fee is not mined, an API call with the VSP is used so it may
// receive and publish the transaction. A follow up on the ticket
// status is scheduled for some time in the future.
err := fp.submitPayment()
fp.mu.Lock()
feeHash := fp.feeHash
fp.mu.Unlock()
var apiErr types.ErrorResponse
if errors.As(err, &apiErr) {
switch apiErr.Code {
case types.ErrFeeAlreadyReceived:
err = w.SetPublished(ctx, &feeHash, true)
if err != nil {
return err
}
err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &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)
if err != nil {
return err
}
// Attempt to create a new fee transaction
fp.mu.Lock()
fp.feeHash = chainhash.Hash{}
fp.feeTx = nil
fp.mu.Unlock()
// err not nilled, so reconcile payment is rescheduled.
default:
// do nothing.
}
}
if err != nil {
// Nothing left to try except trying again.
fp.schedule("reconcile payment", fp.reconcilePayment)
return err
}
err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey)
if err != nil {
return err
}
// confirmPayment will remove the fee payment processing when the fee
// has reached sufficient confirmations, and reschedule itself if the
// fee is not confirmed yet. If the fee tx is ever removed from the
// wallet, this will schedule another reconcile.
return fp.confirmPayment()
/*
// XXX? for each input, c.Wallet.UnlockOutpoint(&outpoint.Hash, outpoint.Index)
// xxx, or let the published tx replace the unpublished one, and unlock
// outpoints as it is processed.
*/
}
func (fp *feePayment) submitPayment() (err error) {
ctx := fp.ctx
w := fp.client.wallet
// stop processing if ticket is expired or spent
if fp.removedExpiredOrSpent() {
// nothing scheduled
return errStopped
}
// 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)
}
if len(feeTx.TxOut) == 0 {
err := fp.makeFeeTx(feeTx)
if err != nil {
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 := make(map[string]string)
agendaChoices, _, err := w.AgendaChoices(ctx, &fp.ticketHash)
if err != nil {
return err
}
for _, agendaChoice := range agendaChoices {
voteChoices[agendaChoice.AgendaID] = agendaChoice.ChoiceID
}
feeTxHex, err := marshalTx(feeTx)
if err != nil {
return err
}
req := types.PayFeeRequest{
Timestamp: time.Now().Unix(),
TicketHash: fp.ticketHash.String(),
FeeTx: feeTxHex,
VotingKey: votingKey,
VoteChoices: voteChoices,
TSpendPolicy: w.TSpendPolicyForTicket(&fp.ticketHash),
TreasuryPolicy: w.TreasuryKeyPolicyForTicket(&fp.ticketHash),
}
_, err = fp.client.PayFee(ctx, req, fp.commitmentAddr)
if err != nil {
var apiErr types.ErrorResponse
if errors.As(err, &apiErr) && apiErr.Code == types.ErrFeeExpired {
// Fee has been expired, so abandon current feetx, set fp.feeTx
// to nil and retry submit payment to make a new fee tx.
feeHash := feeTx.TxHash()
err := w.AbandonTransaction(ctx, &feeHash)
if err != nil {
fp.log.Errorf("error abandoning expired fee tx %v", err)
}
fp.mu.Lock()
fp.feeTx = nil
fp.mu.Unlock()
}
return fmt.Errorf("payfee: %w", err)
}
// TODO - validate server timestamp?
fp.log.Infof("successfully processed %v", fp.ticketHash)
return nil
}
func (fp *feePayment) confirmPayment() (err error) {
ctx := fp.ctx
w := fp.client.wallet
// stop processing if ticket is expired or spent
if fp.removedExpiredOrSpent() {
// nothing scheduled
return errStopped
}
defer func() {
if err != nil && !errors.Is(err, errStopped) {
fp.schedule("reconcile payment", fp.reconcilePayment)
}
}()
status, err := fp.client.status(ctx, &fp.ticketHash)
// Suppress log if the wallet is currently locked.
if err != nil && !errors.Is(err, wallet_errs.Locked) {
fp.log.Warnf("Rescheduling status check for %v: %v", &fp.ticketHash, err)
}
if err != nil {
// Stop processing if the status check cannot be performed, but
// a significant amount of confirmations are observed on the fee
// transaction.
//
// Otherwise, chedule another confirmation check, in case the
// status API can be performed at a later time or more
// confirmations are observed.
fp.mu.Lock()
feeHash := fp.feeHash
fp.mu.Unlock()
confs, err := w.TxConfirms(ctx, &feeHash)
if err != nil {
return err
}
if confs >= 6 {
fp.remove("confirmed")
err = w.UpdateVspTicketFeeToConfirmed(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey)
if err != nil {
return err
}
return nil
}
fp.schedule("confirm payment", fp.confirmPayment)
return nil
}
switch status.FeeTxStatus {
case "received":
// VSP has received the fee tx but has not yet broadcast it.
// VSP will only broadcast the tx when ticket has 6+ confirmations.
fp.schedule("confirm payment", fp.confirmPayment)
return nil
case "broadcast":
fp.log.Infof("VSP has successfully sent the fee tx for %v", &fp.ticketHash)
// Broadcasted, but not confirmed.
fp.schedule("confirm payment", fp.confirmPayment)
return nil
case "confirmed":
fp.remove("confirmed by VSP")
// nothing scheduled
fp.mu.Lock()
feeHash := fp.feeHash
fp.mu.Unlock()
err = w.UpdateVspTicketFeeToConfirmed(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey)
if err != nil {
return err
}
return nil
case "error":
fp.log.Warnf("VSP failed to broadcast feetx for %v -- restarting payment",
&fp.ticketHash)
fp.schedule("reconcile payment", fp.reconcilePayment)
return nil
default:
// XXX put in unknown state
fp.log.Warnf("VSP responded with %v for %v", status.FeeTxStatus,
&fp.ticketHash)
}
return nil
}
func marshalTx(tx *wire.MsgTx) (string, error) {
var buf bytes.Buffer
buf.Grow(tx.SerializeSize() * 2)
err := tx.Serialize(hex.NewEncoder(&buf))
return buf.String(), err
}

View File

@ -1,43 +1,25 @@
module github.com/decred/vspd/client/v2
module github.com/decred/vspd/client/v3
go 1.19
require (
decred.org/dcrwallet/v3 v3.0.1
github.com/decred/dcrd/blockchain/stake/v5 v5.0.0
github.com/decred/dcrd/chaincfg/chainhash v1.0.4
github.com/decred/dcrd/chaincfg/v3 v3.2.0
github.com/decred/dcrd/dcrutil/v4 v4.0.1
github.com/decred/dcrd/txscript/v4 v4.1.0
github.com/decred/dcrd/wire v1.6.0
github.com/decred/slog v1.2.0
github.com/decred/vspd/types/v2 v2.0.0
)
require (
decred.org/cspp/v2 v2.1.0 // indirect
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect
github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a // indirect
github.com/dchest/siphash v1.2.3 // indirect
github.com/decred/base58 v1.0.5 // indirect
github.com/decred/dcrd/blockchain/standalone/v2 v2.2.0 // indirect
github.com/decred/dcrd/chaincfg/chainhash v1.0.4 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/decred/dcrd/crypto/ripemd160 v1.0.2 // indirect
github.com/decred/dcrd/database/v3 v3.0.1 // indirect
github.com/decred/dcrd/dcrec v1.0.1 // indirect
github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/decred/dcrd/dcrjson/v4 v4.0.1 // indirect
github.com/decred/dcrd/gcs/v4 v4.0.0 // indirect
github.com/decred/dcrd/hdkeychain/v3 v3.1.1 // indirect
github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0 // indirect
github.com/decred/go-socks v1.1.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/jrick/bitset v1.0.0 // indirect
github.com/jrick/wsrpc/v2 v2.3.5 // indirect
github.com/decred/dcrd/wire v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.9.0 // indirect
lukechampine.com/blake3 v1.2.1 // indirect
)

View File

@ -1,72 +1,33 @@
decred.org/cspp/v2 v2.1.0 h1:HeHb9+BFqrBaAPc6CsPiUpPFmC1uyBM2mJZUAbUXkRw=
decred.org/cspp/v2 v2.1.0/go.mod h1:9nO3bfvCheOPIFZw5f6sRQ42CjBFB5RKSaJ9Iq6G4MA=
decred.org/dcrwallet/v3 v3.0.1 h1:+OLi+u/MvKc3Ubcnf19oyG/a5hJ/qp4OtezdiQZnLIs=
decred.org/dcrwallet/v3 v3.0.1/go.mod h1:a+R8BZIOKVpWVPat5VZoBWNh/cnIciwcRkPtrzfS/tw=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a h1:clYxJ3Os0EQUKDDVU8M0oipllX0EkuFNBfhVQuIfyF0=
github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a/go.mod h1:z/9Ck1EDixEbBbZ2KH2qNHekEmDLTOZ+FyoIPWWSVOI=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
github.com/decred/base58 v1.0.5 h1:hwcieUM3pfPnE/6p3J100zoRfGkQxBulZHo7GZfOqic=
github.com/decred/base58 v1.0.5/go.mod h1:s/8lukEHFA6bUQQb/v3rjUySJ2hu+RioCzLukAVkrfw=
github.com/decred/dcrd/blockchain/stake/v5 v5.0.0 h1:WyxS8zMvTMpC5qYC9uJY+UzuV/x9ko4z20qBtH5Hzzs=
github.com/decred/dcrd/blockchain/stake/v5 v5.0.0/go.mod h1:5sSjMq9THpnrLkW0SjEqIBIo8qq2nXzc+m7k9oFVVmY=
github.com/decred/dcrd/blockchain/standalone/v2 v2.2.0 h1:v3yfo66axjr3oLihct+5tLEeM9YUzvK3i/6e2Im6RO0=
github.com/decred/dcrd/blockchain/standalone/v2 v2.2.0/go.mod h1:JsOpl2nHhW2D2bWMEtbMuAE+mIU/Pdd1i1pmYR+2RYI=
github.com/decred/dcrd/blockchain/v5 v5.0.0 h1:eAI9zbNpCFR6Xik6RLUEijAL3BO4QVJQ0Az3sz7ZGqk=
github.com/decred/dcrd/chaincfg/chainhash v1.0.4 h1:zRCv6tdncLfLTKYqu7hrXvs7hW+8FO/NvwoFvGsrluU=
github.com/decred/dcrd/chaincfg/chainhash v1.0.4/go.mod h1:hA86XxlBWwHivMvxzXTSD0ZCG/LoYsFdWnCekkTMCqY=
github.com/decred/dcrd/chaincfg/v3 v3.2.0 h1:6WxA92AGBkycEuWvxtZMvA76FbzbkDRoK8OGbsR2muk=
github.com/decred/dcrd/chaincfg/v3 v3.2.0/go.mod h1:2rHW1TKyFmwZTVBLoU/Cmf0oxcpBjUEegbSlBfrsriI=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/crypto/ripemd160 v1.0.2 h1:TvGTmUBHDU75OHro9ojPLK+Yv7gDl2hnUvRocRCjsys=
github.com/decred/dcrd/crypto/ripemd160 v1.0.2/go.mod h1:uGfjDyePSpa75cSQLzNdVmWlbQMBuiJkvXw/MNKRY4M=
github.com/decred/dcrd/database/v3 v3.0.1 h1:oaklASAsUBwDoRgaS961WYqecFMZNhI1k+BmGgeW7/U=
github.com/decred/dcrd/database/v3 v3.0.1/go.mod h1:IErr/Z62pFLoPZTMPGxedbcIuseGk0w3dszP3AFbXyw=
github.com/decred/dcrd/dcrec v1.0.1 h1:gDzlndw0zYxM5BlaV17d7ZJV6vhRe9njPBFeg4Db2UY=
github.com/decred/dcrd/dcrec v1.0.1/go.mod h1:CO+EJd8eHFb8WHa84C7ZBkXsNUIywaTHb+UAuI5uo6o=
github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 h1:l/lhv2aJCUignzls81+wvga0TFlyoZx8QxRMQgXpZik=
github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3/go.mod h1:AKpV6+wZ2MfPRJnTbQ6NPgWrKzbe9RCIlCF/FKzMtM8=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/decred/dcrd/dcrjson/v4 v4.0.1 h1:vyQuB1miwGqbCVNm8P6br3V65WQ6wyrh0LycMkvaBBg=
github.com/decred/dcrd/dcrjson/v4 v4.0.1/go.mod h1:2qVikafVF9/X3PngQVmqkbUbyAl32uik0k/kydgtqMc=
github.com/decred/dcrd/dcrutil/v4 v4.0.1 h1:E+d2TNbpOj0f1L9RqkZkEm1QolFjajvkzxWC5WOPf1s=
github.com/decred/dcrd/dcrutil/v4 v4.0.1/go.mod h1:7EXyHYj8FEqY+WzMuRkF0nh32ueLqhutZDoW4eQ+KRc=
github.com/decred/dcrd/gcs/v4 v4.0.0 h1:bet+Ax1ZFUqn2M0g1uotm0b8F6BZ9MmblViyJ088E8k=
github.com/decred/dcrd/gcs/v4 v4.0.0/go.mod h1:9z+EBagzpEdAumwS09vf/hiGaR8XhNmsBgaVq6u7/NI=
github.com/decred/dcrd/hdkeychain/v3 v3.1.1 h1:4WhyHNBy7ec6qBUC7Fq7JFVGSd7bpuR5H+AJRID8Lyk=
github.com/decred/dcrd/hdkeychain/v3 v3.1.1/go.mod h1:HaabrLc27lnny5/Ph9+6I3szp0op5MCb7smEwlzfD60=
github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0 h1:4YUKsWKrKlkhVMYGRB6G0XI6QfwUnwEH18eoEbM1/+M=
github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0/go.mod h1:dDHO7ivrPAhZjFD3LoOJN/kdq5gi0sxie6zCsWHAiUo=
github.com/decred/dcrd/txscript/v4 v4.1.0 h1:uEdcibIOl6BuWj3AqmXZ9xIK/qbo6lHY9aNk29FtkrU=
github.com/decred/dcrd/txscript/v4 v4.1.0/go.mod h1:OVguPtPc4YMkgssxzP8B6XEMf/J3MB6S1JKpxgGQqi0=
github.com/decred/dcrd/wire v1.6.0 h1:YOGwPHk4nzGr6OIwUGb8crJYWDiVLpuMxfDBCCF7s/o=
github.com/decred/dcrd/wire v1.6.0/go.mod h1:XQ8Xv/pN/3xaDcb7sH8FBLS9cdgVctT7HpBKKGsIACk=
github.com/decred/go-socks v1.1.0 h1:dnENcc0KIqQo3HSXdgboXAHgqsCIutkqq6ntQjYtm2U=
github.com/decred/go-socks v1.1.0/go.mod h1:sDhHqkZH0X4JjSa02oYOGhcGHYp12FsY1jQ/meV8md0=
github.com/decred/slog v1.2.0 h1:soHAxV52B54Di3WtKLfPum9OFfWqwtf/ygf9njdfnPM=
github.com/decred/slog v1.2.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0=
github.com/decred/vspd/types/v2 v2.0.0 h1:FaPA+W4OOMRWK+Vk4fyyYdXoVLRMMRQsxzsnSjJjOnI=
github.com/decred/vspd/types/v2 v2.0.0/go.mod h1:2xnNqedkt9GuL+pK8uIzDxqYxFlwLRflYFJH64b76n0=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jrick/bitset v1.0.0 h1:Ws0PXV3PwXqWK2n7Vz6idCdrV/9OrBXgHEJi27ZB9Dw=
github.com/jrick/bitset v1.0.0/go.mod h1:ZOYB5Uvkla7wIEY4FEssPVi3IQXa02arznRaYaAEPe4=
github.com/jrick/wsrpc/v2 v2.3.5 h1:CwdycaR/df09iGkPMXs1FxqAHMCQbdAiTGoHfOrtuds=
github.com/jrick/wsrpc/v2 v2.3.5/go.mod h1:7oBeDM/xMF6Yqy4GDAjpppuOf1hm6lWsaG3EaMrm+aA=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=