diff --git a/.golangci.yml b/.golangci.yml index 321e4d0..2492e50 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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 diff --git a/client/autoclient.go b/client/autoclient.go deleted file mode 100644 index 879fc3b..0000000 --- a/client/autoclient.go +++ /dev/null @@ -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 -} diff --git a/client/feepayment.go b/client/feepayment.go deleted file mode 100644 index f16e724..0000000 --- a/client/feepayment.go +++ /dev/null @@ -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 -} diff --git a/client/go.mod b/client/go.mod index dbe190b..6bfcc93 100644 --- a/client/go.mod +++ b/client/go.mod @@ -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 ) diff --git a/client/go.sum b/client/go.sum index 08b7c49..233aa0e 100644 --- a/client/go.sum +++ b/client/go.sum @@ -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=