From a5003c046bb9b3ff732277b596eca177716d6883 Mon Sep 17 00:00:00 2001 From: Jamie Holdstock Date: Tue, 30 May 2023 11:50:23 +0100 Subject: [PATCH] client: Automatic fee payment from dcrwallet (#382) --- .golangci.yml | 15 + client/autoclient.go | 447 ++++++++++++++++++++ client/client.go | 4 + client/client_test.go | 4 + client/feepayment.go | 937 ++++++++++++++++++++++++++++++++++++++++++ client/go.mod | 39 +- client/go.sum | 123 +++++- go.mod | 16 +- go.sum | 31 +- 9 files changed, 1580 insertions(+), 36 deletions(-) create mode 100644 client/autoclient.go create mode 100644 client/feepayment.go diff --git a/.golangci.yml b/.golangci.yml index 2492e50..321e4d0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -27,3 +27,18 @@ 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 new file mode 100644 index 0000000..879fc3b --- /dev/null +++ b/client/autoclient.go @@ -0,0 +1,447 @@ +// 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/client.go b/client/client.go index e621c0a..619f62b 100644 --- a/client/client.go +++ b/client/client.go @@ -1,3 +1,7 @@ +// 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 ( diff --git a/client/client_test.go b/client/client_test.go index 5fca36a..0fc4f82 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1,3 +1,7 @@ +// 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 ( diff --git a/client/feepayment.go b/client/feepayment.go new file mode 100644 index 0000000..f16e724 --- /dev/null +++ b/client/feepayment.go @@ -0,0 +1,937 @@ +// 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 fa30d7a..e839b26 100644 --- a/client/go.mod +++ b/client/go.mod @@ -3,20 +3,47 @@ module github.com/decred/vspd/client/v2 go 1.19 require ( + decred.org/dcrwallet/v3 v3.0.0-20230519033517-96817277627d + 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.1.2 + github.com/decred/dcrd/dcrutil/v4 v4.0.0 github.com/decred/dcrd/txscript/v4 v4.0.0 + github.com/decred/dcrd/wire v1.5.0 github.com/decred/slog v1.2.0 github.com/decred/vspd/types/v2 v2.0.0 ) +replace ( + github.com/decred/dcrd/blockchain/stake/v5 => github.com/decred/dcrd/blockchain/stake/v5 v5.0.0-20221022042529-0a0cc3b3bf92 + github.com/decred/dcrd/blockchain/standalone/v2 => github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230430213532-f95870f9c6af + github.com/decred/dcrd/chaincfg/v3 => github.com/decred/dcrd/chaincfg/v3 v3.1.2-0.20230412145739-9aa79ec168f6 + github.com/decred/dcrd/gcs/v4 => github.com/decred/dcrd/gcs/v4 v4.0.0-20221022042529-0a0cc3b3bf92 +) + require ( + decred.org/cspp/v2 v2.0.0 // indirect github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect - github.com/dchest/siphash v1.2.2 // indirect - github.com/decred/base58 v1.0.3 // indirect - github.com/decred/dcrd/chaincfg/chainhash v1.0.3 // indirect - github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect + github.com/companyzero/sntrup4591761 v0.0.0-20200131011700-2b0d299dbd22 // indirect + github.com/dchest/siphash v1.2.3 // indirect + github.com/decred/base58 v1.0.4 // indirect + github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230430213532-f95870f9c6af // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/crypto/ripemd160 v1.0.1 // indirect + github.com/decred/dcrd/database/v3 v3.0.0 // indirect github.com/decred/dcrd/dcrec v1.0.0 // indirect github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect - github.com/decred/dcrd/wire v1.5.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.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.0 // 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 + go.etcd.io/bbolt v1.3.7 // indirect + golang.org/x/crypto v0.6.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.5.0 // indirect ) diff --git a/client/go.sum b/client/go.sum index 751015f..f78e843 100644 --- a/client/go.sum +++ b/client/go.sum @@ -1,31 +1,140 @@ +decred.org/cspp/v2 v2.0.0 h1:b4fZrElRufz30rYnBZ2shhC8AjNVTN4i6TMzDi+hk44= +decred.org/cspp/v2 v2.0.0/go.mod h1:0shJWKTWY3LxZEWGxtbER1Y45+HVjC0WZtj4bctSzCI= +decred.org/dcrwallet/v3 v3.0.0-20230519033517-96817277627d h1:Gov8SXYPsw6eyIBhzLP6+kjfDHdv6Q/ifIMt8fKtoYc= +decred.org/dcrwallet/v3 v3.0.0-20230519033517-96817277627d/go.mod h1:JGNFyWBroylUUf973eIHsdHN7PuJdCUVkIOMyWVv+y0= 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-20200131011700-2b0d299dbd22 h1:vfqLMkB1UqwJliW0I/34oscQawInrVfL1uPjGEEt2YY= +github.com/companyzero/sntrup4591761 v0.0.0-20200131011700-2b0d299dbd22/go.mod h1:LoZJNGDWmVPqMEHmeJzj4Weq4Stjc6FKY6FVpY3Hem0= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dchest/siphash v1.2.2 h1:9DFz8tQwl9pTVt5iok/9zKyzA1Q6bRGiF3HPiEEVr9I= github.com/dchest/siphash v1.2.2/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= -github.com/decred/base58 v1.0.3 h1:KGZuh8d1WEMIrK0leQRM47W85KqCAdl2N+uagbctdDI= +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.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbiSM78E= +github.com/decred/base58 v1.0.4 h1:QJC6B0E0rXOPA8U/kw2rP+qiRJsUaE2Er+pYb3siUeA= +github.com/decred/base58 v1.0.4/go.mod h1:jJswKPEdvpFpvf7dsDvFZyLT22xZ9lWqEByX38oGd9E= +github.com/decred/dcrd/blockchain/stake/v5 v5.0.0-20221022042529-0a0cc3b3bf92 h1:GTIg6r54cgNhUyZMNmTsmxM8OneEJ8t6QcWQCqu+al0= +github.com/decred/dcrd/blockchain/stake/v5 v5.0.0-20221022042529-0a0cc3b3bf92/go.mod h1:fij5xS9IBfJ5e/F5ytp/g/TWjrETEMXUFlE6C7KYOvA= +github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230430213532-f95870f9c6af h1:tmfTIkxkq7NdeLj3mrMk0uEdRyU1/oO1MWx8dfwOmm8= +github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230430213532-f95870f9c6af/go.mod h1:PpM/jdMaD5MnBcSoFd+rJZE4q8tU0xPTTAyVzgegLQI= +github.com/decred/dcrd/blockchain/v5 v5.0.0-20221022042529-0a0cc3b3bf92 h1:AYgHfuWXQh5NTv3mciQleTm2K1IuM7ozgfXy1Vg6V7k= github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= -github.com/decred/dcrd/chaincfg/chainhash v1.0.3 h1:PF2czcYZGW3dz4i/35AUfVAgnqHl9TMNQt1ADTYGOoE= github.com/decred/dcrd/chaincfg/chainhash v1.0.3/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= -github.com/decred/dcrd/chaincfg/v3 v3.1.0 h1:u8l+E6ryv8E0WY69pM/lUI36UeAVcLKBwD/Q3xPiuog= -github.com/decred/dcrd/chaincfg/v3 v3.1.0/go.mod h1:4XF9nlx2NeGD4xzw1+L0DGICZMl0a5rKV8nnuHLgk8o= -github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +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.1.2-0.20230412145739-9aa79ec168f6 h1:rKAzrv3gIEQaEQpnWU4IKxXrvx6QfXkdiOUKLvwEpQw= +github.com/decred/dcrd/chaincfg/v3 v3.1.2-0.20230412145739-9aa79ec168f6/go.mod h1:aEEti0kQSBFAlzHln4FB+3L30k9ZN1M7YDfYuK5VWtc= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +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.1 h1:TjRL4LfftzTjXzaufov96iDAkbY2R3aTvH2YMYa1IOc= github.com/decred/dcrd/crypto/ripemd160 v1.0.1/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg= +github.com/decred/dcrd/database/v3 v3.0.0 h1:7VVN2sWjKB934jvXzjnyGJFUVH9d8Qh5VULi+NMRjek= +github.com/decred/dcrd/database/v3 v3.0.0/go.mod h1:8EyKddB8rXDi6/CDOdYc/7qL1//sb6iwg9DctP0ZJF4= github.com/decred/dcrd/dcrec v1.0.0 h1:W+z6Es+Rai3MXYVoPAxYr5U1DGis0Co33scJ6uH2J6o= github.com/decred/dcrd/dcrec v1.0.0/go.mod h1:HIaqbEJQ+PDzQcORxnqen5/V1FR3B4VpIfmePklt8Q8= github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2 h1:bX7rtGTMBDJxujZ29GNqtn7YCAdINjHKnA6J6tBBv6s= github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= +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.0 h1:AY00fWy/ETrMHN0DNV3XUbH1aip2RG1AoTy5dp0+sJE= +github.com/decred/dcrd/dcrutil/v4 v4.0.0/go.mod h1:QQpX5WVH3/ixVtiW15xZMe+neugXX3l2bsrYgq6nz4M= +github.com/decred/dcrd/gcs/v4 v4.0.0-20221022042529-0a0cc3b3bf92 h1:DiAdpAQg54JL5iWFdB+DEdK/xuKIjTyL6mlR7iIumPQ= +github.com/decred/dcrd/gcs/v4 v4.0.0-20221022042529-0a0cc3b3bf92/go.mod h1:2SpSpCW0vOWlACQNAn7mPuIb3Vet070zfs1SpcSEv8o= +github.com/decred/dcrd/hdkeychain/v3 v3.1.0 h1:NlUjzPMzexbk1PyJu6vrQaiilep5WsEPB0KdhLYrEcE= +github.com/decred/dcrd/hdkeychain/v3 v3.1.0/go.mod h1:rDCdqwGkcTfEyRheG1g8Wc38appT2C9+D1XTlLy21lo= +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.0.0 h1:BwaBUCMCmg58MCYoBhxVjL8ZZKUIfoJuxu/djmh8h58= github.com/decred/dcrd/txscript/v4 v4.0.0/go.mod h1:OJtxNc5RqwQyfrRnG2gG8uMeNPo8IAJp+TD1UKXkqk8= github.com/decred/dcrd/wire v1.5.0 h1:3SgcEzSjqAMQvOugP0a8iX7yQSpiVT1yNi9bc4iOXVg= github.com/decred/dcrd/wire v1.5.0/go.mod h1:fzAjVqw32LkbAZIt5mnrvBR751GTa3e0rRQdOIhPY3w= +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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +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.4/go.mod h1:XPYs8BnRWl99lCvXRM5SLpZmTPqWpSOPkDIqYTwDPfU= +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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/go.mod b/go.mod index 739983b..2fb550f 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ require ( decred.org/dcrwallet/v3 v3.0.0 github.com/decred/dcrd/blockchain/stake/v5 v5.0.0 github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1 - github.com/decred/dcrd/chaincfg/chainhash v1.0.3 - github.com/decred/dcrd/chaincfg/v3 v3.1.1 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 + github.com/decred/dcrd/chaincfg/chainhash v1.0.4 + github.com/decred/dcrd/chaincfg/v3 v3.1.2 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 github.com/decred/dcrd/dcrutil/v4 v4.0.0 github.com/decred/dcrd/hdkeychain/v3 v3.1.0 github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0 @@ -28,23 +28,23 @@ require ( ) replace ( - decred.org/dcrwallet/v3 => decred.org/dcrwallet/v3 v3.0.0-20230406144806-dc82294b976a + decred.org/dcrwallet/v3 => decred.org/dcrwallet/v3 v3.0.0-20230519033517-96817277627d github.com/decred/dcrd/blockchain/stake/v5 => github.com/decred/dcrd/blockchain/stake/v5 v5.0.0-20221022042529-0a0cc3b3bf92 - github.com/decred/dcrd/blockchain/standalone/v2 => github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230411184711-ce46220cf772 + github.com/decred/dcrd/blockchain/standalone/v2 => github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230430213532-f95870f9c6af + github.com/decred/dcrd/chaincfg/v3 => github.com/decred/dcrd/chaincfg/v3 v3.1.2-0.20230412145739-9aa79ec168f6 github.com/decred/dcrd/gcs/v4 => github.com/decred/dcrd/gcs/v4 v4.0.0-20221022042529-0a0cc3b3bf92 - github.com/decred/dcrd/rpc/jsonrpc/types/v4 => github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0-20221022042529-0a0cc3b3bf92 ) require ( github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect github.com/dchest/siphash v1.2.3 // indirect github.com/decred/base58 v1.0.4 // indirect - github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/crypto/ripemd160 v1.0.1 // indirect github.com/decred/dcrd/database/v3 v3.0.0 // indirect github.com/decred/dcrd/dcrec v1.0.0 // indirect github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2 // indirect - github.com/decred/dcrd/dcrjson/v4 v4.0.0 // indirect + github.com/decred/dcrd/dcrjson/v4 v4.0.1 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect diff --git a/go.sum b/go.sum index fe7946c..bb939c9 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -decred.org/dcrwallet/v3 v3.0.0-20230406144806-dc82294b976a h1:gssWH7pL1maf4qOGt2vkIcaXvz8GVAvR9sltU6W7JOQ= -decred.org/dcrwallet/v3 v3.0.0-20230406144806-dc82294b976a/go.mod h1:hXxn7XBmvCZQ4D0uMdMVCzAsV258WoDIl+/UrABEB4o= +decred.org/dcrwallet/v3 v3.0.0-20230519033517-96817277627d h1:Gov8SXYPsw6eyIBhzLP6+kjfDHdv6Q/ifIMt8fKtoYc= +decred.org/dcrwallet/v3 v3.0.0-20230519033517-96817277627d/go.mod h1:JGNFyWBroylUUf973eIHsdHN7PuJdCUVkIOMyWVv+y0= 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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -14,16 +14,17 @@ github.com/decred/base58 v1.0.4 h1:QJC6B0E0rXOPA8U/kw2rP+qiRJsUaE2Er+pYb3siUeA= github.com/decred/base58 v1.0.4/go.mod h1:jJswKPEdvpFpvf7dsDvFZyLT22xZ9lWqEByX38oGd9E= github.com/decred/dcrd/blockchain/stake/v5 v5.0.0-20221022042529-0a0cc3b3bf92 h1:GTIg6r54cgNhUyZMNmTsmxM8OneEJ8t6QcWQCqu+al0= github.com/decred/dcrd/blockchain/stake/v5 v5.0.0-20221022042529-0a0cc3b3bf92/go.mod h1:fij5xS9IBfJ5e/F5ytp/g/TWjrETEMXUFlE6C7KYOvA= -github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230411184711-ce46220cf772 h1:LNE2EluIv1R4R933HJhS8rpgOqfpPAeAKkPq0Bf7cP8= -github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230411184711-ce46220cf772/go.mod h1:PpM/jdMaD5MnBcSoFd+rJZE4q8tU0xPTTAyVzgegLQI= +github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230430213532-f95870f9c6af h1:tmfTIkxkq7NdeLj3mrMk0uEdRyU1/oO1MWx8dfwOmm8= +github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230430213532-f95870f9c6af/go.mod h1:PpM/jdMaD5MnBcSoFd+rJZE4q8tU0xPTTAyVzgegLQI= github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= -github.com/decred/dcrd/chaincfg/chainhash v1.0.3 h1:PF2czcYZGW3dz4i/35AUfVAgnqHl9TMNQt1ADTYGOoE= github.com/decred/dcrd/chaincfg/chainhash v1.0.3/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= -github.com/decred/dcrd/chaincfg/v3 v3.1.0/go.mod h1:4XF9nlx2NeGD4xzw1+L0DGICZMl0a5rKV8nnuHLgk8o= -github.com/decred/dcrd/chaincfg/v3 v3.1.1 h1:Ki8kq5IXGmjriiQyPCrCTF1aZSBiORb91/Sr5xW4otw= -github.com/decred/dcrd/chaincfg/v3 v3.1.1/go.mod h1:4XF9nlx2NeGD4xzw1+L0DGICZMl0a5rKV8nnuHLgk8o= -github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +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.1.2-0.20230412145739-9aa79ec168f6 h1:rKAzrv3gIEQaEQpnWU4IKxXrvx6QfXkdiOUKLvwEpQw= +github.com/decred/dcrd/chaincfg/v3 v3.1.2-0.20230412145739-9aa79ec168f6/go.mod h1:aEEti0kQSBFAlzHln4FB+3L30k9ZN1M7YDfYuK5VWtc= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +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.1 h1:TjRL4LfftzTjXzaufov96iDAkbY2R3aTvH2YMYa1IOc= github.com/decred/dcrd/crypto/ripemd160 v1.0.1/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg= github.com/decred/dcrd/database/v3 v3.0.0 h1:7VVN2sWjKB934jvXzjnyGJFUVH9d8Qh5VULi+NMRjek= @@ -33,16 +34,16 @@ github.com/decred/dcrd/dcrec v1.0.0/go.mod h1:HIaqbEJQ+PDzQcORxnqen5/V1FR3B4VpIf github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2 h1:bX7rtGTMBDJxujZ29GNqtn7YCAdINjHKnA6J6tBBv6s= github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= -github.com/decred/dcrd/dcrjson/v4 v4.0.0 h1:KsaFhHAYO+vLYz7Qmx/fs1gOY5ouTEz8hRuDm8jmJtU= -github.com/decred/dcrd/dcrjson/v4 v4.0.0/go.mod h1:DMnSpU8lsVh+Nt5kHl63tkrjBDA7UIs4+ov8Kwwgvjs= +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.0 h1:AY00fWy/ETrMHN0DNV3XUbH1aip2RG1AoTy5dp0+sJE= github.com/decred/dcrd/dcrutil/v4 v4.0.0/go.mod h1:QQpX5WVH3/ixVtiW15xZMe+neugXX3l2bsrYgq6nz4M= github.com/decred/dcrd/hdkeychain/v3 v3.1.0 h1:NlUjzPMzexbk1PyJu6vrQaiilep5WsEPB0KdhLYrEcE= github.com/decred/dcrd/hdkeychain/v3 v3.1.0/go.mod h1:rDCdqwGkcTfEyRheG1g8Wc38appT2C9+D1XTlLy21lo= -github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0-20221022042529-0a0cc3b3bf92 h1:R32+XN8qM6kB7qUHfbkskPAldTPXWIhxMk+e3UBNwyY= -github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0-20221022042529-0a0cc3b3bf92/go.mod h1:x1zG8D4HRmUCDtAioDe/QQ/PazzFXcYtSrbasX3FHoE= +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.0.0 h1:BwaBUCMCmg58MCYoBhxVjL8ZZKUIfoJuxu/djmh8h58= github.com/decred/dcrd/txscript/v4 v4.0.0/go.mod h1:OJtxNc5RqwQyfrRnG2gG8uMeNPo8IAJp+TD1UKXkqk8= github.com/decred/dcrd/wire v1.5.0 h1:3SgcEzSjqAMQvOugP0a8iX7yQSpiVT1yNi9bc4iOXVg=