From 2db761e0727866fda0bc303432e443599a2d3b45 Mon Sep 17 00:00:00 2001 From: jholdstock Date: Wed, 9 Aug 2023 09:49:29 +0100 Subject: [PATCH] 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. --- .golangci.yml | 15 - client/autoclient.go | 447 --------------------- client/feepayment.go | 937 ------------------------------------------- client/go.mod | 24 +- client/go.sum | 39 -- 5 files changed, 3 insertions(+), 1459 deletions(-) delete mode 100644 client/autoclient.go delete mode 100644 client/feepayment.go 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=