From ae020c8f84a15e7444522f87bc50d0c9cb5c6f4a Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Wed, 9 Sep 2020 16:27:04 +0000 Subject: [PATCH] Move address definitions to payments package The wallet.NetworkBackend interface is now able to use dcrwallet's own Address interface rather than using dcrutil in the definition. This required moving the interfaces and several implementations to be moved from the wallet package to a new payments package to avoid a package import cycle. The old wallet.KnownAddress interface is renamed to wallet.Address since it is no longer confused with the previous wallet.Address interface (which becomes a payments interface). --- config.go | 11 +- internal/cfgutil/address.go | 16 +- internal/loader/loader.go | 9 +- internal/rpc/jsonrpc/marshaling.go | 2 +- internal/rpc/jsonrpc/methods.go | 112 +++---- internal/rpc/rpcserver/server.go | 108 +++---- payments/address.go | 427 +++++++++++++++++++++++++ rpc/client/dcrd/calls.go | 7 +- rpc/jsonrpc/types/methods.go | 14 - spv/backend.go | 21 +- spv/rescan.go | 13 +- ticketbuyer/tb.go | 5 +- vsp/feeaddress.go | 21 +- vsp/payfee.go | 18 +- vsp/ticketstatus.go | 3 +- vsp/vsp.go | 7 +- wallet/addresses.go | 482 +++++++++++++---------------- wallet/addresses_test.go | 9 +- wallet/chainntfns.go | 9 +- wallet/coinjoin.go | 37 +-- wallet/createtx.go | 216 ++++++------- wallet/discovery.go | 18 +- wallet/discovery_test.go | 7 +- wallet/mixing.go | 35 +-- wallet/network.go | 3 +- wallet/network_test.go | 3 +- wallet/rescan.go | 97 ++---- wallet/udb/txmined.go | 13 +- wallet/unstable.go | 17 +- wallet/wallet.go | 224 +++++++++----- 30 files changed, 1141 insertions(+), 823 deletions(-) create mode 100644 payments/address.go diff --git a/config.go b/config.go index f009ddd72..3de702860 100644 --- a/config.go +++ b/config.go @@ -22,6 +22,7 @@ import ( "decred.org/dcrwallet/errors" "decred.org/dcrwallet/internal/cfgutil" "decred.org/dcrwallet/internal/netparams" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/version" "decred.org/dcrwallet/wallet" "decred.org/dcrwallet/wallet/txrules" @@ -93,7 +94,7 @@ type config struct { EnableVoting bool `long:"enablevoting" description:"Automatically create votes and revocations"` PurchaseAccount string `long:"purchaseaccount" description:"Account to autobuy tickets from"` PoolAddress *cfgutil.AddressFlag `long:"pooladdress" description:"VSP fee address"` - poolAddress dcrutil.Address + poolAddress payments.Address PoolFees float64 `long:"poolfees" description:"VSP fee percentage (1.00 equals 1.00% fee)"` GapLimit uint32 `long:"gaplimit" description:"Allowed unused address gap between used addresses of accounts"` StakePoolColdExtKey string `long:"stakepoolcoldextkey" description:"xpub:maxindex for fee addresses (VSP-only option)"` @@ -163,7 +164,7 @@ type config struct { type ticketBuyerOptions struct { BalanceToMaintainAbsolute *cfgutil.AmountFlag `long:"balancetomaintainabsolute" description:"Amount of funds to keep in wallet when purchasing tickets"` VotingAddress *cfgutil.AddressFlag `long:"votingaddress" description:"Purchase tickets with voting rights assigned to this address"` - votingAddress dcrutil.Address + votingAddress payments.Address Limit uint `long:"limit" description:"Buy no more than specified number of tickets per block (0 disables limit)"` VotingAccount string `long:"votingaccount" description:"Account used to derive addresses specifying voting rights"` } @@ -345,7 +346,7 @@ func loadConfig(ctx context.Context) (*config, []string, error) { StakePoolColdExtKey: defaultStakePoolColdExtKey, AllowHighFees: defaultAllowHighFees, RelayFee: cfgutil.NewAmountFlag(txrules.DefaultRelayFeePerKb), - PoolAddress: cfgutil.NewAddressFlag(), + PoolAddress: new(cfgutil.AddressFlag), AccountGapLimit: defaultAccountGapLimit, DisableCoinTypeUpgrades: defaultDisableCoinTypeUpgrades, CircuitLimit: defaultCircuitLimit, @@ -353,7 +354,7 @@ func loadConfig(ctx context.Context) (*config, []string, error) { // Ticket Buyer Options TBOpts: ticketBuyerOptions{ BalanceToMaintainAbsolute: cfgutil.NewAmountFlag(defaultBalanceToMaintainAbsolute), - VotingAddress: cfgutil.NewAddressFlag(), + VotingAddress: new(cfgutil.AddressFlag), }, } @@ -468,7 +469,7 @@ func loadConfig(ctx context.Context) (*config, []string, error) { // Check that no addresses were created for the wrong network for _, a := range []struct { flag *cfgutil.AddressFlag - addr *dcrutil.Address + addr *payments.Address }{ {cfg.PoolAddress, &cfg.poolAddress}, {cfg.TBOpts.VotingAddress, &cfg.TBOpts.votingAddress}, diff --git a/internal/cfgutil/address.go b/internal/cfgutil/address.go index 4c03b8508..ebaef40c6 100644 --- a/internal/cfgutil/address.go +++ b/internal/cfgutil/address.go @@ -1,25 +1,21 @@ // Copyright (c) 2015-2016 The btcsuite developers -// Copyright (c) 2016 The Decred developers +// Copyright (c) 2016-2020 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package cfgutil import ( + "decred.org/dcrwallet/payments" "github.com/decred/dcrd/dcrutil/v3" ) -// AddressFlag contains a dcrutil.Address and implements the flags.Marshaler and -// Unmarshaler interfaces so it can be used as a config struct field. +// AddressFlag implements the flags.Marshaler and Unmarshaler interfaces +// and returns parsed addresses from the configured values (if any). type AddressFlag struct { str string } -// NewAddressFlag creates an AddressFlag with a default dcrutil.Address. -func NewAddressFlag() *AddressFlag { - return new(AddressFlag) -} - // MarshalFlag satisfies the flags.Marshaler interface. func (a *AddressFlag) MarshalFlag() (string, error) { return a.str, nil @@ -33,9 +29,9 @@ func (a *AddressFlag) UnmarshalFlag(addr string) error { // Address decodes the address flag for the network described by params. // If the flag is the empty string, this returns a nil address. -func (a *AddressFlag) Address(params dcrutil.AddressParams) (dcrutil.Address, error) { +func (a *AddressFlag) Address(params dcrutil.AddressParams) (payments.Address, error) { if a.str == "" { return nil, nil } - return dcrutil.DecodeAddress(a.str, params) + return payments.DecodeAddress(a.str, params) } diff --git a/internal/loader/loader.go b/internal/loader/loader.go index 3617fa0d9..2f84f50d2 100644 --- a/internal/loader/loader.go +++ b/internal/loader/loader.go @@ -12,6 +12,7 @@ import ( "sync" "decred.org/dcrwallet/errors" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/wallet" _ "decred.org/dcrwallet/wallet/drivers/bdb" // driver loaded during init "github.com/decred/dcrd/chaincfg/v3" @@ -51,9 +52,8 @@ type Loader struct { // StakeOptions contains the various options necessary for stake mining. type StakeOptions struct { VotingEnabled bool - AddressReuse bool - VotingAddress dcrutil.Address - PoolAddress dcrutil.Address + VotingAddress payments.Address + PoolAddress payments.Address PoolFees float64 StakePoolColdExtKey string } @@ -170,7 +170,6 @@ func (l *Loader) CreateWatchingOnlyWallet(ctx context.Context, extendedPubKey st DB: db, PubPassphrase: pubPass, VotingEnabled: so.VotingEnabled, - AddressReuse: so.AddressReuse, VotingAddress: so.VotingAddress, PoolAddress: so.PoolAddress, PoolFees: so.PoolFees, @@ -261,7 +260,6 @@ func (l *Loader) CreateNewWallet(ctx context.Context, pubPassphrase, privPassphr DB: db, PubPassphrase: pubPassphrase, VotingEnabled: so.VotingEnabled, - AddressReuse: so.AddressReuse, VotingAddress: so.VotingAddress, PoolAddress: so.PoolAddress, PoolFees: so.PoolFees, @@ -321,7 +319,6 @@ func (l *Loader) OpenExistingWallet(ctx context.Context, pubPassphrase []byte) ( DB: db, PubPassphrase: pubPassphrase, VotingEnabled: so.VotingEnabled, - AddressReuse: so.AddressReuse, VotingAddress: so.VotingAddress, PoolAddress: so.PoolAddress, PoolFees: so.PoolFees, diff --git a/internal/rpc/jsonrpc/marshaling.go b/internal/rpc/jsonrpc/marshaling.go index ae7959d63..3361c9faa 100644 --- a/internal/rpc/jsonrpc/marshaling.go +++ b/internal/rpc/jsonrpc/marshaling.go @@ -35,7 +35,7 @@ func addressArrayMarshaler(n int, s func(i int) string) json.Marshaler { }) } -func knownAddressMarshaler(addrs []wallet.KnownAddress) json.Marshaler { +func walletAddressMarshaler(addrs []wallet.Address) json.Marshaler { return addressArrayMarshaler(len(addrs), func(i int) string { return addrs[i].String() }) diff --git a/internal/rpc/jsonrpc/methods.go b/internal/rpc/jsonrpc/methods.go index 62b81329a..7a544b50e 100644 --- a/internal/rpc/jsonrpc/methods.go +++ b/internal/rpc/jsonrpc/methods.go @@ -22,6 +22,7 @@ import ( "decred.org/dcrwallet/errors" "decred.org/dcrwallet/p2p" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/rpc/client/dcrd" "decred.org/dcrwallet/rpc/jsonrpc/types" "decred.org/dcrwallet/spv" @@ -366,16 +367,22 @@ func walletPubKeys(ctx context.Context, w *wallet.Wallet, keys []string) ([]*dcr pubKeyAddrs := make([]*dcrutil.AddressSecpPubKey, len(keys)) for i, key := range keys { - addr, err := decodeAddress(key, w.ChainParams()) + // Raw pubkeys aren't used in Decred, only their encoded address format. + utilAddr, err := dcrutil.DecodeAddress(key, w.ChainParams()) if err != nil { - return nil, err + return nil, rpcErrorf(dcrjson.ErrRPCInvalidAddressOrKey, + "Address %q: %v", key, err) } - switch addr := addr.(type) { + switch addr := utilAddr.(type) { case *dcrutil.AddressSecpPubKey: pubKeyAddrs[i] = addr continue } + addr, err := payments.WrapUtilAddress(utilAddr) + if err != nil { + return nil, err + } a, err := w.KnownAddress(ctx, addr) if err != nil { if errors.Is(err, errors.NotExist) { @@ -500,7 +507,7 @@ func (s *Server) auditReuse(ctx context.Context, icmd interface{}) (interface{}, } for i := 1; i < len(ticket.TxOut); i += 2 { // iterate commitments out := ticket.TxOut[i] - addr, err := stake.AddrFromSStxPkScrCommitment(out.PkScript, params) + addr, err := payments.ParseTicketCommitmentAddress(out.PkScript, params) if err != nil { return false, err } @@ -559,7 +566,7 @@ func (s *Server) consolidate(ctx context.Context, icmd interface{}) (interface{} } // Set change address if specified. - var changeAddr dcrutil.Address + var changeAddr payments.Address if cmd.Address != nil { if *cmd.Address != "" { addr, err := decodeAddress(*cmd.Address, w.ChainParams()) @@ -663,28 +670,12 @@ func (s *Server) createRawTransaction(ctx context.Context, icmd interface{}) (in for encodedAddr, amount := range cmd.Amounts { // Decode the provided address. This also ensures the network encoded // with the address matches the network the server is currently on. - addr, err := dcrutil.DecodeAddress(encodedAddr, s.activeNet) + addr, err := payments.DecodeAddress(encodedAddr, s.activeNet) if err != nil { return nil, rpcErrorf(dcrjson.ErrRPCInvalidAddressOrKey, "Address %q: %v", encodedAddr, err) } - // Ensure the address is one of the supported types. - switch addr.(type) { - case *dcrutil.AddressPubKeyHash: - case *dcrutil.AddressScriptHash: - default: - return nil, rpcErrorf(dcrjson.ErrRPCInvalidAddressOrKey, - "Invalid type: %T", addr) - } - - // Create a new script which pays to the provided address. - pkScript, err := txscript.PayToAddrScript(addr) - if err != nil { - return nil, rpcErrorf(dcrjson.ErrRPCInternal.Code, - "Pay to address script: %v", err) - } - atomic, err := dcrutil.NewAmount(amount) if err != nil { return nil, rpcErrorf(dcrjson.ErrRPCInternal.Code, @@ -696,7 +687,12 @@ func (s *Server) createRawTransaction(ctx context.Context, icmd interface{}) (in "Amount outside valid range: %v", atomic) } - txOut := wire.NewTxOut(int64(atomic), pkScript) + version, pkScript := addr.PaymentScript() + txOut := &wire.TxOut{ + Version: version, + PkScript: pkScript, + Value: int64(atomic), + } mtx.AddTxOut(txOut) } @@ -979,7 +975,7 @@ func (s *Server) getAddressesByAccount(ctx context.Context, icmd interface{}) (i if err != nil { return nil, err } - return knownAddressMarshaler(addrs), nil + return walletAddressMarshaler(addrs), nil } account, err := w.AccountNumber(ctx, cmd.Account) @@ -1278,7 +1274,7 @@ func (s *Server) getInfo(ctx context.Context, icmd interface{}) (interface{}, er return info, nil } -func decodeAddress(s string, params *chaincfg.Params) (dcrutil.Address, error) { +func decodeAddress(s string, params *chaincfg.Params) (payments.Address, error) { // Secp256k1 pubkey as a string, handle differently. if len(s) == 66 || len(s) == 130 { pubKeyBytes, err := hex.DecodeString(s) @@ -1290,11 +1286,10 @@ func decodeAddress(s string, params *chaincfg.Params) (dcrutil.Address, error) { if err != nil { return nil, err } - - return pubKeyAddr, nil + return payments.WrapUtilAddress(pubKeyAddr) } - addr, err := dcrutil.DecodeAddress(s, params) + addr, err := payments.DecodeAddress(s, params) if err != nil { return nil, rpcErrorf(dcrjson.ErrRPCInvalidAddressOrKey, "invalid address %q: decode failed: %#q", s, err) @@ -1347,7 +1342,7 @@ func (s *Server) getAccountAddress(ctx context.Context, icmd interface{}) (inter } return nil, err } - addr, err := w.CurrentAddress(account) + addr, err := w.CurrentAddress(ctx, account) if err != nil { // Expect account lookup to succeed if errors.Is(err, errors.NotExist) { @@ -1356,7 +1351,7 @@ func (s *Server) getAccountAddress(ctx context.Context, icmd interface{}) (inter return nil, err } - return addr.Address(), nil + return addr.String(), nil } // getUnconfirmedBalance handles a getunconfirmedbalance extension request @@ -1689,7 +1684,7 @@ func (s *Server) getNewAddress(ctx context.Context, icmd interface{}) (interface if err != nil { return nil, err } - return addr.Address(), nil + return addr.String(), nil } // getRawChangeAddress handles a getrawchangeaddress request by creating @@ -1722,7 +1717,7 @@ func (s *Server) getRawChangeAddress(ctx context.Context, icmd interface{}) (int } // Return the new payment address string. - return addr.Address(), nil + return addr.String(), nil } // getReceivedByAccount handles a getreceivedbyaccount request by returning @@ -2459,16 +2454,16 @@ func (s *Server) listAddressTransactions(ctx context.Context, icmd interface{}) } // Decode addresses. - hash160Map := make(map[string]struct{}) + addrSet := make(map[string]struct{}) for _, addrStr := range cmd.Addresses { addr, err := decodeAddress(addrStr, w.ChainParams()) if err != nil { return nil, err } - hash160Map[string(addr.ScriptAddress())] = struct{}{} + addrSet[addr.String()] = struct{}{} } - return w.ListAddressTransactions(ctx, hash160Map) + return w.ListAddressTransactions(ctx, addrSet) } // listAllTransactions handles a listalltransactions request by returning @@ -2507,7 +2502,7 @@ func (s *Server) listUnspent(ctx context.Context, icmd interface{}) (interface{} if err != nil { return nil, err } - addresses[a.Address()] = struct{}{} + addresses[a.String()] = struct{}{} } } @@ -2595,7 +2590,7 @@ func (s *Server) purchaseTicket(ctx context.Context, icmd interface{}) (interfac } // Set ticket address if specified. - var ticketAddr dcrutil.Address + var ticketAddr payments.Address if cmd.TicketAddress != nil { if *cmd.TicketAddress != "" { addr, err := decodeAddress(*cmd.TicketAddress, w.ChainParams()) @@ -2614,7 +2609,7 @@ func (s *Server) purchaseTicket(ctx context.Context, icmd interface{}) (interfac } // Set pool address if specified. - var poolAddr dcrutil.Address + var poolAddr payments.Address var poolFee float64 if cmd.PoolAddress != nil { if *cmd.PoolAddress != "" { @@ -2729,12 +2724,7 @@ func makeOutputs(pairs map[string]dcrutil.Amount, chainParams *chaincfg.Params) if err != nil { return nil, err } - - pkScript, vers, err := addressScript(addr) - if err != nil { - return nil, err - } - + vers, pkScript := addr.PaymentScript() outputs = append(outputs, &wire.TxOut{ Value: int64(amt), PkScript: pkScript, @@ -2795,7 +2785,7 @@ func (s *Server) redeemMultiSigOut(ctx context.Context, icmd interface{}) (inter // Convert the address to a useable format. If // we have no address, create a new address in // this wallet to send the output to. - var addr dcrutil.Address + var addr payments.Address var err error if cmd.Address != nil { addr, err = decodeAddress(*cmd.Address, w.ChainParams()) @@ -2835,10 +2825,7 @@ func (s *Server) redeemMultiSigOut(ctx context.Context, icmd interface{}) (inter txIn := wire.NewTxIn(&op, int64(p2shOutput.OutputAmount), nil) msgTx.AddTxIn(txIn) - pkScript, _, err := addressScript(addr) - if err != nil { - return nil, err - } + _, pkScript := addr.PaymentScript() err = w.PrepareRedeemMultiSigOutTxOutput(&msgTx, p2shOutput, &pkScript) if err != nil { @@ -2903,11 +2890,7 @@ func (s *Server) redeemMultiSigOuts(ctx context.Context, icmd interface{}) (inte if err != nil { return nil, err } - p2shAddr, ok := addr.(*dcrutil.AddressScriptHash) - if !ok { - return nil, rpcErrorf(dcrjson.ErrRPCInvalidParameter, "address is not P2SH") - } - msos, err := wallet.UnstableAPI(w).UnspentMultisigCreditsForAddress(ctx, p2shAddr) + msos, err := wallet.UnstableAPI(w).UnspentMultisigCreditsForAddress(ctx, addr) if err != nil { return nil, err } @@ -3307,7 +3290,7 @@ func (s *Server) sendToMultiSig(ctx context.Context, icmd interface{}) (interfac result := &types.SendToMultiSigResult{ TxHash: tx.MsgTx.TxHash().String(), - Address: addr.Address(), + Address: addr.String(), RedeemScript: hex.EncodeToString(script), } @@ -3814,7 +3797,7 @@ func (s *Server) validateAddress(ctx context.Context, icmd interface{}) (interfa // by checking the type of "addr", however, the reference // implementation only puts that information if the script is // "ismine", and we follow that behaviour. - result.Address = addr.Address() + result.Address = addr.String() result.IsValid = true ka, err := w.KnownAddress(ctx, addr) @@ -3893,7 +3876,7 @@ func (s *Server) verifyMessage(ctx context.Context, icmd interface{}) (interface var valid bool // Decode address and base64 signature from the request. - addr, err := dcrutil.DecodeAddress(cmd.Address, s.activeNet) + addr, err := decodeAddress(cmd.Address, s.activeNet) if err != nil { return nil, err } @@ -3902,25 +3885,10 @@ func (s *Server) verifyMessage(ctx context.Context, icmd interface{}) (interface return nil, err } - // Addresses must have an associated secp256k1 private key and therefore - // must be P2PK or P2PKH (P2SH is not allowed). - switch a := addr.(type) { - case *dcrutil.AddressSecpPubKey: - case *dcrutil.AddressPubKeyHash: - if a.DSA() != dcrec.STEcdsaSecp256k1 { - goto WrongAddrKind - } - default: - goto WrongAddrKind - } - valid, err = wallet.VerifyMessage(cmd.Message, addr, sig, s.activeNet) // Mirror Bitcoin Core behavior, which treats all erorrs as an invalid // signature. return err == nil && valid, nil - -WrongAddrKind: - return nil, rpcErrorf(dcrjson.ErrRPCInvalidParameter, "address must be secp256k1 P2PK or P2PKH") } // version handles the version command by returning the RPC API versions of the diff --git a/internal/rpc/rpcserver/server.go b/internal/rpc/rpcserver/server.go index a1ddff594..68568aad8 100644 --- a/internal/rpc/rpcserver/server.go +++ b/internal/rpc/rpcserver/server.go @@ -36,6 +36,7 @@ import ( "decred.org/dcrwallet/internal/loader" "decred.org/dcrwallet/internal/netparams" "decred.org/dcrwallet/p2p" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/rpc/client/dcrd" pb "decred.org/dcrwallet/rpc/walletrpc" "decred.org/dcrwallet/spv" @@ -50,7 +51,6 @@ import ( "github.com/decred/dcrd/blockchain/stake/v3" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/chaincfg/v3" - "github.com/decred/dcrd/dcrec" "github.com/decred/dcrd/dcrutil/v3" "github.com/decred/dcrd/gcs/v2" "github.com/decred/dcrd/hdkeychain/v3" @@ -121,8 +121,8 @@ func errorCode(err error) codes.Code { // decodeAddress decodes an address and verifies it is intended for the active // network. This should be used preferred to direct usage of // dcrutil.DecodeAddress, which does not perform the network check. -func decodeAddress(a string, params *chaincfg.Params) (dcrutil.Address, error) { - addr, err := dcrutil.DecodeAddress(a, params) +func decodeAddress(a string, params *chaincfg.Params) (payments.Address, error) { + addr, err := payments.DecodeAddress(a, params) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid address %v: %v", a, err) } @@ -479,7 +479,7 @@ func (s *walletServer) NextAddress(ctx context.Context, req *pb.NextAddressReque } var ( - addr dcrutil.Address + addr payments.Address err error ) switch req.Kind { @@ -512,7 +512,7 @@ func (s *walletServer) NextAddress(ctx context.Context, req *pb.NextAddressReque } return &pb.NextAddressResponse{ - Address: addr.Address(), + Address: addr.String(), PublicKey: pubKeyAddrString, }, nil } @@ -576,27 +576,33 @@ func (s *walletServer) ImportScript(ctx context.Context, // TODO: Rather than assuming the "default" version, it must be a parameter // to the request. - sc, addrs, requiredSigs, err := txscript.ExtractPkScriptAddrs( - 0, req.Script, s.wallet.ChainParams()) + const defaultVersion = 0 + addr, err := payments.ParseAddress(defaultVersion, req.Script, s.wallet.ChainParams()) if err != nil && req.RequireRedeemable { return nil, status.Errorf(codes.FailedPrecondition, "The script is not redeemable by the wallet") } - ownAddrs := 0 - for _, a := range addrs { - haveAddr, err := s.wallet.HaveAddress(ctx, a) - if err != nil { - return nil, translateError(err) + var multisigRedeemable bool + switch addr := addr.(type) { + case payments.MultisigAddress: + pubkeys := addr.PubKeys() + m := addr.M() + var own int + for i := range pubkeys { + ok, err := s.wallet.HavePubkey(ctx, pubkeys[i]) + if err != nil { + return nil, err + } + if ok { + own++ + } } - if haveAddr { - ownAddrs++ + multisigRedeemable = own >= m + if !multisigRedeemable && req.RequireRedeemable { + return nil, status.Errorf(codes.FailedPrecondition, + "The script is not redeemable by the wallet") } } - redeemable := sc == txscript.MultiSigTy && ownAddrs >= requiredSigs - if !redeemable && req.RequireRedeemable { - return nil, status.Errorf(codes.FailedPrecondition, - "The script is not redeemable by the wallet") - } if req.ScanFrom < 0 { return nil, status.Errorf(codes.InvalidArgument, @@ -626,7 +632,11 @@ func (s *walletServer) ImportScript(ctx context.Context, return nil, translateError(err) } - return &pb.ImportScriptResponse{P2ShAddress: p2sh.String(), Redeemable: redeemable}, nil + resp := &pb.ImportScriptResponse{ + P2ShAddress: p2sh.String(), + Redeemable: multisigRedeemable, + } + return resp, nil } func (s *walletServer) Balance(ctx context.Context, req *pb.BalanceRequest) ( @@ -919,10 +929,7 @@ func (s *walletServer) FundTransaction(ctx context.Context, req *pb.FundTransact if err != nil { return nil, translateError(err) } - changeScript, _, err = addressScript(changeAddr) - if err != nil { - return nil, translateError(err) - } + _, changeScript = changeAddr.PaymentScript() } return &pb.FundTransactionResponse{ @@ -946,10 +953,7 @@ func decodeDestination(dest *pb.ConstructTransactionRequest_OutputDestination, if err != nil { return nil, 0, err } - pkScript, version, err = addressScript(addr) - if err != nil { - return nil, 0, translateError(err) - } + version, pkScript = addr.PaymentScript() return pkScript, version, nil case dest.Script != nil: if dest.ScriptVersion > uint32(^uint16(0)) { @@ -1566,7 +1570,7 @@ func (s *walletServer) PurchaseTickets(ctx context.Context, minConf := int32(req.RequiredConfirmations) params := s.wallet.ChainParams() - var ticketAddr dcrutil.Address + var ticketAddr payments.Address var err error n, err := s.wallet.NetworkBackend() @@ -1581,7 +1585,7 @@ func (s *walletServer) PurchaseTickets(ctx context.Context, } } - var poolAddr dcrutil.Address + var poolAddr payments.Address var poolFees float64 if req.PoolAddress != "" { poolAddr, err = decodeAddress(req.PoolAddress, params) @@ -1822,14 +1826,11 @@ func (s *walletServer) signMessage(ctx context.Context, address, message string) // Addresses must have an associated secp256k1 private key and therefore // must be P2PK or P2PKH (P2SH is not allowed). var sig []byte - switch a := addr.(type) { - case *dcrutil.AddressSecpPubKey: - case *dcrutil.AddressPubKeyHash: - if a.DSA() != dcrec.STEcdsaSecp256k1 { - goto WrongAddrKind - } + switch addr.(type) { + case payments.PubKeyHashAddress, payments.PubKeyAddress: default: - goto WrongAddrKind + return nil, status.Error(codes.InvalidArgument, + "address must be secp256k1 P2PK or P2PKH") } sig, err = s.wallet.SignMessage(ctx, message, addr) @@ -1837,10 +1838,6 @@ func (s *walletServer) signMessage(ctx context.Context, address, message string) return nil, translateError(err) } return sig, nil - -WrongAddrKind: - return nil, status.Error(codes.InvalidArgument, - "address must be secp256k1 P2PK or P2PKH") } func (s *walletServer) SignMessage(ctx context.Context, req *pb.SignMessageRequest) (*pb.SignMessageResponse, error) { @@ -2417,7 +2414,7 @@ func (t *ticketbuyerV2Server) RunTicketBuyer(req *pb.RunTicketBuyerRequest, svr // Legacy vsp request. After stopping supporting the old vsp version, this // code can be removed. // Confirm validity of provided voting addresses and pool addresses. - var votingAddress dcrutil.Address + var votingAddress payments.Address var err error if req.VotingAddress != "" { votingAddress, err = decodeAddress(req.VotingAddress, params) @@ -2425,7 +2422,7 @@ func (t *ticketbuyerV2Server) RunTicketBuyer(req *pb.RunTicketBuyerRequest, svr return err } } - var poolAddress dcrutil.Address + var poolAddress payments.Address if req.PoolAddress != "" { if req.VspHost != "" || req.VspPubkey != "" { return status.Errorf(codes.InvalidArgument, @@ -3096,21 +3093,18 @@ func (s *messageVerificationServer) VerifyMessage(ctx context.Context, req *pb.V var valid bool - addr, err := dcrutil.DecodeAddress(req.Address, s.chainParams) + addr, err := decodeAddress(req.Address, s.chainParams) if err != nil { return nil, translateError(err) } // Addresses must have an associated secp256k1 private key and therefore // must be P2PK or P2PKH (P2SH is not allowed). - switch a := addr.(type) { - case *dcrutil.AddressSecpPubKey: - case *dcrutil.AddressPubKeyHash: - if a.DSA() != dcrec.STEcdsaSecp256k1 { - goto WrongAddrKind - } + switch addr.(type) { + case payments.PubKeyHashAddress, payments.PubKeyAddress: default: - goto WrongAddrKind + return nil, status.Error(codes.InvalidArgument, + "address must be secp256k1 P2PK or P2PKH") } valid, err = wallet.VerifyMessage(req.Message, addr, req.Signature, s.chainParams) @@ -3118,9 +3112,6 @@ func (s *messageVerificationServer) VerifyMessage(ctx context.Context, req *pb.V return nil, translateError(err) } return &pb.VerifyMessageResponse{Valid: valid}, nil - -WrongAddrKind: - return nil, status.Error(codes.InvalidArgument, "address must be secp256k1 P2PK or P2PKH") } // StartDecodeMessageService starts the MessageDecode service @@ -3271,13 +3262,8 @@ func (s *walletServer) SignHashes(ctx context.Context, req *pb.SignHashesRequest // Addresses must have an associated secp256k1 private key and therefore // must be P2PK or P2PKH (P2SH is not allowed). - switch a := addr.(type) { - case *dcrutil.AddressSecpPubKey: - case *dcrutil.AddressPubKeyHash: - if a.DSA() != dcrec.STEcdsaSecp256k1 { - return nil, status.Error(codes.InvalidArgument, - "address must be secp256k1 P2PK or P2PKH") - } + switch addr.(type) { + case payments.PubKeyHashAddress, payments.PubKeyAddress: default: return nil, status.Error(codes.InvalidArgument, "address must be secp256k1 P2PK or P2PKH") diff --git a/payments/address.go b/payments/address.go new file mode 100644 index 000000000..22894d41d --- /dev/null +++ b/payments/address.go @@ -0,0 +1,427 @@ +// Copyright (c) 2020 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package payments + +import ( + "encoding/binary" + + "decred.org/dcrwallet/errors" + "decred.org/dcrwallet/wallet/txsizes" + "github.com/decred/dcrd/blockchain/stake/v3" + "github.com/decred/dcrd/dcrec" + "github.com/decred/dcrd/dcrutil/v3" + "github.com/decred/dcrd/txscript/v3" +) + +// Address is a human-readable encoding of an output script. +// +// Address encodings may include a network identifier, to prevent misuse on an +// alternate Decred network. +type Address interface { + String() string + + // PaymentScript returns the output script and script version to pay the + // address. The version is always returned with the script, as it is + // not useful to use the script without the version. + PaymentScript() (version uint16, script []byte) + + // ScriptLen returns the known length of the address output script. + ScriptLen() int +} + +// StakeAddress is an address kind which is allowed by consensus rules to appear +// in special positions in stake transactions. These rules permit P2PKH and +// P2SH outputs only. Stake scripts require a special opcode "tag" to appear at +// the start of the script to classify the type of stake transaction. +type StakeAddress interface { + String() string + + VoteRights() (script []byte, version uint16) + TicketChange() (script []byte, version uint16) + RewardCommitment(amount dcrutil.Amount, limits uint16) (script []byte, version uint16) + PayVoteCommitment() (script []byte, version uint16) + PayRevokeCommitment() (script []byte, version uint16) +} + +// PubKeyHashAddress is an address which pays to the hash of a public key. +type PubKeyHashAddress interface { + Address + + PubKeyHash() []byte +} + +// PubKeyAddress is an address which pays to a public key. These addresses are +// typically avoided in favor of pubkey hash addresses, unless an operation +// (e.g. generating a multisig script) requires knowing the unhashed public key. +type PubKeyAddress interface { + Address + + PubKey() []byte +} + +// ScriptHashAddress is an address which pays to the hash of a redeem script +// (pay-to-script-hash, or P2SH). The redeem script is provided by the +// redeeming input scripts as a final data push. +// +// An implementation of ScriptHashAddress does not necessarily know the unhashed +// redeem script, and may only know how to pay to the address. +type ScriptHashAddress interface { + Address + + ScriptHash() []byte +} + +// MultisigAddress is a P2SH address with a known multisig redeem script. A +// multisig output requires M out of any N valid signatures from N different +// keypairs in order to redeem the output. +// +// Bare multisig output scripts are nonstandard, and so this is only able to +// implement Address as a P2SH address. Any P2SH address with an unknown redeem +// script may be a multisig address, but it is impossible to know until the +// redeem script is revealed. +type MultisigAddress interface { + ScriptHashAddress + + // RedeemScript returns the unhashed multisig script that redeemers must + // provide as the final data push + RedeemScript() []byte + + // PubKeys returns all N public keys for each keypair. + PubKeys() [][]byte + + // M is the required number of unique signatures needed to redeem + // outputs paying the address. + M() int + + // N is the total number of public/private keypairs. The return value + // must always equal len(PubKeys()). + N() int +} + +// wrappedUtilAddr implements Address for any non-P2PKH and non-P2SH +// address. +type wrappedUtilAddr struct { + addr dcrutil.Address + script []byte +} + +var _ Address = (*wrappedUtilAddr)(nil) + +func (x *wrappedUtilAddr) String() string { + return x.addr.Address() +} +func (x *wrappedUtilAddr) PaymentScript() (version uint16, script []byte) { + return 0, x.script +} +func (x *wrappedUtilAddr) ScriptLen() int { + return len(x.script) +} +func (x *wrappedUtilAddr) utilAddr() dcrutil.Address { + return x.addr +} + +// WrapUtilAddress wraps a dcrutil.Address with a wallet implementation of the +// Address interface. +func WrapUtilAddress(addr dcrutil.Address) (Address, error) { + switch addr := addr.(type) { + case *dcrutil.AddressPubKeyHash: + return &p2pkhAddress{addr}, nil + case *dcrutil.AddressScriptHash: + return &p2shAddress{addr}, nil + default: + script, err := txscript.PayToAddrScript(addr) + if err != nil { + return nil, err + } + return &wrappedUtilAddr{addr, script}, nil + } +} + +// AddressToUtilAddress unwraps or creates a new implementation of a +// dcrutil.Address from an Address. +func AddressToUtilAddress(a Address, params dcrutil.AddressParams) (dcrutil.Address, error) { + switch a := a.(type) { + case interface{ utilAddr() dcrutil.Address }: + return a.utilAddr(), nil + } + return dcrutil.DecodeAddress(a.String(), params) +} + +// DecodeAddress decodes an address in string encoding for the network described +// by params. The wallet depends on addresses implementing Address +// returned by this func, rather than external implementations of the interface. +func DecodeAddress(s string, params dcrutil.AddressParams) (Address, error) { + utilAddr, err := dcrutil.DecodeAddress(s, params) + if err != nil { + return nil, err + } + return WrapUtilAddress(utilAddr) +} + +// ParseAddress parses an address (if the script form matches a known address +// type) from a transaction's output script. This function works with both +// regular and stake-tagged output scripts. +// +// If the script is multisig, the returned address implements the +// MultisigAddress P2SH type. +func ParseAddress(vers uint16, script []byte, params dcrutil.AddressParams) (Address, error) { + _, addrs, m, err := txscript.ExtractPkScriptAddrs(vers, script, params) + if err != nil { + return nil, err + } + if m >= 2 && m <= len(addrs) { // multisig + n := len(addrs) + pubkeyAddrs := make([]*dcrutil.AddressSecpPubKey, 0, n) + pubkeys := make([][]byte, 0, n) + for i := range addrs { + addr := addrs[i] + pubkeyAddr, ok := addr.(*dcrutil.AddressSecpPubKey) + if !ok { + err := errors.Errorf("unexpected address type %T", addr) + return nil, err + } + pubkey := pubkeyAddr.PubKey().SerializeCompressed() + + pubkeyAddrs = append(pubkeyAddrs, pubkeyAddr) + pubkeys = append(pubkeys, pubkey) + } + redeemScript, err := txscript.MultiSigScript(pubkeyAddrs, m) + if err != nil { + return nil, errors.Errorf("create multisig redeem script: %v", err) + } + p2shUtilAddr, err := dcrutil.NewAddressScriptHash(redeemScript, params) + if err != nil { + return nil, errors.Errorf("create multisig P2SH: %v", err) + } + + addr := new(multisigAddress) + addr.p2shAddress = p2shAddress{p2shUtilAddr} + addr.redeemScript = redeemScript + addr.pubkeys = pubkeys + addr.m = m + return addr, nil + } + if len(addrs) != 1 { + return nil, errors.E("expected one address") + } + addr := addrs[0] + return WrapUtilAddress(addr) +} + +// ParseTicketCommitmentAddress parses a P2PKH or P2SH address from a ticket's +// commitment output script. +func ParseTicketCommitmentAddress(script []byte, params dcrutil.AddressParams) (Address, error) { + utilAddr, err := stake.AddrFromSStxPkScrCommitment(script, params) + if err != nil { + return nil, err + } + switch a := utilAddr.(type) { + case *dcrutil.AddressPubKeyHash: + return &p2pkhAddress{a}, nil + case *dcrutil.AddressScriptHash: + return &p2shAddress{a}, nil + default: + return nil, errors.Errorf("unknown address type %T", utilAddr) + } +} + +type p2pkhAddress struct { + *dcrutil.AddressPubKeyHash +} + +var _ interface { + Address + PubKeyHashAddress + StakeAddress +} = (*p2pkhAddress)(nil) + +// P2PKHAddress creates an address which is paid to by the hash of its public +// key. +func P2PKHAddress(pubkeyHash []byte, params dcrutil.AddressParams) (PubKeyHashAddress, error) { + utilAddr, err := dcrutil.NewAddressPubKeyHash(pubkeyHash, params, dcrec.STEcdsaSecp256k1) + if err != nil { + return nil, err + } + return &p2pkhAddress{utilAddr}, nil +} + +func (x *p2pkhAddress) PubKeyHash() []byte { return x.Hash160()[:] } +func (x *p2pkhAddress) utilAddr() dcrutil.Address { return x.AddressPubKeyHash } +func (x *p2pkhAddress) ScriptLen() int { return txsizes.P2PKHPkScriptSize } + +func (x *p2pkhAddress) PaymentScript() (uint16, []byte) { + s := []byte{ + 0: txscript.OP_DUP, + 1: txscript.OP_HASH160, + 2: txscript.OP_DATA_20, + 23: txscript.OP_EQUALVERIFY, + 24: txscript.OP_CHECKSIG, + } + copy(s[3:23], x.Hash160()[:]) + return 0, s +} + +func (x *p2pkhAddress) VoteRights() (script []byte, version uint16) { + s := []byte{ + 0: txscript.OP_SSTX, + 1: txscript.OP_DUP, + 2: txscript.OP_HASH160, + 3: txscript.OP_DATA_20, + 24: txscript.OP_EQUALVERIFY, + 25: txscript.OP_CHECKSIG, + } + copy(s[4:24], x.Hash160()[:]) + return s, 0 +} + +func (x *p2pkhAddress) TicketChange() (script []byte, version uint16) { + s := []byte{ + 0: txscript.OP_SSTXCHANGE, + 1: txscript.OP_DUP, + 2: txscript.OP_HASH160, + 3: txscript.OP_DATA_20, + 24: txscript.OP_EQUALVERIFY, + 25: txscript.OP_CHECKSIG, + } + copy(s[4:24], x.Hash160()[:]) + return s, 0 +} + +func (x *p2pkhAddress) RewardCommitment(amount dcrutil.Amount, limits uint16) ([]byte, uint16) { + s := make([]byte, 32) + s[0] = txscript.OP_RETURN + s[1] = txscript.OP_DATA_30 + copy(s[2:22], x.Hash160()[:]) + binary.LittleEndian.PutUint64(s[22:30], uint64(amount)) + binary.LittleEndian.PutUint16(s[30:32], limits) + return s, 0 +} + +func (x *p2pkhAddress) PayVoteCommitment() (script []byte, version uint16) { + s := []byte{ + 0: txscript.OP_SSGEN, + 1: txscript.OP_DUP, + 2: txscript.OP_HASH160, + 3: txscript.OP_DATA_20, + 24: txscript.OP_EQUALVERIFY, + 25: txscript.OP_CHECKSIG, + } + copy(s[4:24], x.Hash160()[:]) + return s, 0 +} + +func (x *p2pkhAddress) PayRevokeCommitment() (script []byte, version uint16) { + s := []byte{ + 0: txscript.OP_SSRTX, + 1: txscript.OP_DUP, + 2: txscript.OP_HASH160, + 3: txscript.OP_DATA_20, + 24: txscript.OP_EQUALVERIFY, + 25: txscript.OP_CHECKSIG, + } + copy(s[4:24], x.Hash160()[:]) + return s, 0 +} + +type p2shAddress struct { + *dcrutil.AddressScriptHash +} + +var _ interface { + Address + ScriptHashAddress + StakeAddress +} = (*p2shAddress)(nil) + +// P2SHAddress creates an address which is paid to by the hash of its redeem +// script. +func P2SHAddress(scriptHash []byte, params dcrutil.AddressParams) (ScriptHashAddress, error) { + utilAddr, err := dcrutil.NewAddressScriptHash(scriptHash, params) + if err != nil { + return nil, err + } + return &p2shAddress{utilAddr}, nil +} + +func (x *p2shAddress) ScriptHash() []byte { return x.Hash160()[:] } +func (x *p2shAddress) utilAddr() dcrutil.Address { return x.AddressScriptHash } +func (x *p2shAddress) ScriptLen() int { return txsizes.P2SHPkScriptSize } + +func (x *p2shAddress) PaymentScript() (uint16, []byte) { + s := []byte{ + 0: txscript.OP_HASH160, + 1: txscript.OP_DATA_20, + 22: txscript.OP_EQUALVERIFY, + } + copy(s[2:22], x.Hash160()[:]) + return 0, s +} + +func (x *p2shAddress) VoteRights() (script []byte, version uint16) { + s := []byte{ + 0: txscript.OP_SSTX, + 1: txscript.OP_HASH160, + 2: txscript.OP_DATA_20, + 23: txscript.OP_EQUALVERIFY, + } + copy(s[3:23], x.Hash160()[:]) + return s, 0 +} + +func (x *p2shAddress) TicketChange() (script []byte, version uint16) { + s := []byte{ + 0: txscript.OP_SSTXCHANGE, + 1: txscript.OP_HASH160, + 2: txscript.OP_DATA_20, + 23: txscript.OP_EQUALVERIFY, + } + copy(s[3:23], x.Hash160()[:]) + return s, 0 +} + +func (x *p2shAddress) RewardCommitment(amount dcrutil.Amount, limits uint16) ([]byte, uint16) { + s := make([]byte, 32) + s[0] = txscript.OP_RETURN + s[1] = txscript.OP_DATA_30 + copy(s[2:22], x.Hash160()[:]) + binary.LittleEndian.PutUint64(s[22:30], uint64(amount)) + binary.LittleEndian.PutUint16(s[30:32], limits) + s[29] |= 0x80 // mark the hash160 as a script hash + return s, 0 +} + +func (x *p2shAddress) PayVoteCommitment() (script []byte, version uint16) { + s := []byte{ + 0: txscript.OP_SSGEN, + 1: txscript.OP_HASH160, + 2: txscript.OP_DATA_20, + 23: txscript.OP_EQUALVERIFY, + } + copy(s[3:23], x.Hash160()[:]) + return s, 0 +} + +func (x *p2shAddress) PayRevokeCommitment() (script []byte, version uint16) { + s := []byte{ + 0: txscript.OP_SSRTX, + 1: txscript.OP_HASH160, + 2: txscript.OP_DATA_20, + 23: txscript.OP_EQUALVERIFY, + } + copy(s[3:23], x.Hash160()[:]) + return s, 0 +} + +type multisigAddress struct { + p2shAddress + redeemScript []byte + pubkeys [][]byte + m int +} + +func (a *multisigAddress) RedeemScript() []byte { return a.redeemScript } +func (a *multisigAddress) PubKeys() [][]byte { return a.pubkeys } +func (a *multisigAddress) M() int { return a.m } +func (a *multisigAddress) N() int { return len(a.pubkeys) } diff --git a/rpc/client/dcrd/calls.go b/rpc/client/dcrd/calls.go index aefef6118..176f4555c 100644 --- a/rpc/client/dcrd/calls.go +++ b/rpc/client/dcrd/calls.go @@ -13,6 +13,7 @@ import ( "strings" "decred.org/dcrwallet/errors" + "decred.org/dcrwallet/payments" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/dcrutil/v3" "github.com/decred/dcrd/gcs/v2" @@ -51,7 +52,7 @@ func hashSliceToStrings(hashes []*chainhash.Hash) []string { return s } -func addrSliceToStrings(addrs []dcrutil.Address) []string { +func addrSliceToStrings(addrs []payments.Address) []string { s := make([]string, len(addrs)) for i, a := range addrs { s[i] = a.String() @@ -127,7 +128,7 @@ func (r *RPC) ExistsExpiredMissedTickets(ctx context.Context, tickets []*chainha // UsedAddresses returns a bitset identifying whether each address has been // publically used on the blockchain. This feature requires the optional dcrd // existsaddress index to be enabled. -func (r *RPC) UsedAddresses(ctx context.Context, addrs []dcrutil.Address) (bitset.Bytes, error) { +func (r *RPC) UsedAddresses(ctx context.Context, addrs []payments.Address) (bitset.Bytes, error) { const op errors.Op = "dcrd.UsedAddresses" addrArray, _ := json.Marshal(addrSliceToStrings(addrs)) var bits bitset.Bytes @@ -316,7 +317,7 @@ func (r *RPC) Headers(ctx context.Context, blockLocators []*chainhash.Hash, hash // LoadTxFilter loads or reloads the precise server-side transaction filter used // for relevant transaction notifications and rescans. // Addresses and outpoints are added to an existing filter if reload is false. -func (r *RPC) LoadTxFilter(ctx context.Context, reload bool, addrs []dcrutil.Address, outpoints []wire.OutPoint) error { +func (r *RPC) LoadTxFilter(ctx context.Context, reload bool, addrs []payments.Address, outpoints []wire.OutPoint) error { const op errors.Op = "dcrd.LoadTxFilter" type outpoint struct { diff --git a/rpc/jsonrpc/types/methods.go b/rpc/jsonrpc/types/methods.go index b362f1e62..b8a03e070 100644 --- a/rpc/jsonrpc/types/methods.go +++ b/rpc/jsonrpc/types/methods.go @@ -147,19 +147,6 @@ func NewCreateNewAccountCmd(account string) *CreateNewAccountCmd { } } -// CreateVotingAccountCmd is a type for handling custom marshaling and -// unmarshalling of createvotingaccount JSON-RPC command. -type CreateVotingAccountCmd struct { - Name string - PubKey string - ChildIndex *uint32 `jsonrpcdefault:"0"` -} - -// NewCreateVotingAccountCmd creates a new CreateVotingAccountCmd. -func NewCreateVotingAccountCmd(name, pubKey string, childIndex *uint32) *CreateVotingAccountCmd { - return &CreateVotingAccountCmd{name, pubKey, childIndex} -} - // DumpPrivKeyCmd defines the dumpprivkey JSON-RPC command. type DumpPrivKeyCmd struct { Address string @@ -1107,7 +1094,6 @@ func init() { {"createmultisig", (*CreateMultisigCmd)(nil)}, {"createsignature", (*CreateSignatureCmd)(nil)}, {"createnewaccount", (*CreateNewAccountCmd)(nil)}, - {"createvotingaccount", (*CreateVotingAccountCmd)(nil)}, {"discoverusage", (*DiscoverUsageCmd)(nil)}, {"dumpprivkey", (*DumpPrivKeyCmd)(nil)}, {"fundrawtransaction", (*FundRawTransactionCmd)(nil)}, diff --git a/spv/backend.go b/spv/backend.go index 9daf58e27..c84a2d16a 100644 --- a/spv/backend.go +++ b/spv/backend.go @@ -11,12 +11,12 @@ import ( "decred.org/dcrwallet/errors" "decred.org/dcrwallet/p2p" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/validate" "decred.org/dcrwallet/wallet" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/dcrutil/v3" "github.com/decred/dcrd/gcs/v2" - "github.com/decred/dcrd/txscript/v3" "github.com/decred/dcrd/wire" ) @@ -104,27 +104,16 @@ func (s *Syncer) String() string { // NOTE: due to blockcf2 *not* including the spent outpoints in the block, the // addrs[] slice MUST include the addresses corresponding to the respective // outpoints, otherwise they will not be returned during the rescan. -func (s *Syncer) LoadTxFilter(ctx context.Context, reload bool, addrs []dcrutil.Address, outpoints []wire.OutPoint) error { +func (s *Syncer) LoadTxFilter(ctx context.Context, reload bool, addrs []payments.Address, outpoints []wire.OutPoint) error { s.filterMu.Lock() if reload || s.rescanFilter == nil { s.rescanFilter = wallet.NewRescanFilter(nil, nil) s.filterData = nil } for _, addr := range addrs { - var pkScript []byte - type scripter interface { - PaymentScript() (uint16, []byte) - } - switch addr := addr.(type) { - case scripter: - _, pkScript = addr.PaymentScript() - default: - pkScript, _ = txscript.PayToAddrScript(addr) - } - if pkScript != nil { - s.rescanFilter.AddAddress(addr) - s.filterData.AddRegularPkScript(pkScript) - } + _, pkScript := addr.PaymentScript() + s.rescanFilter.AddAddress(addr) + s.filterData.AddRegularPkScript(pkScript) } for i := range outpoints { s.rescanFilter.AddUnspentOutPoint(&outpoints[i]) diff --git a/spv/rescan.go b/spv/rescan.go index 4bf4f560d..057d22667 100644 --- a/spv/rescan.go +++ b/spv/rescan.go @@ -6,6 +6,7 @@ package spv import ( + "decred.org/dcrwallet/payments" "github.com/decred/dcrd/blockchain/stake/v3" "github.com/decred/dcrd/gcs/v2/blockcf2" "github.com/decred/dcrd/txscript/v3" @@ -60,7 +61,11 @@ func (s *Syncer) rescanCheckTransactions(matches *[]*wire.MsgTx, fadded *blockcf continue } for _, a := range addrs { - if !s.rescanFilter.ExistsAddress(a) { + addr, err := payments.WrapUtilAddress(a) + if err != nil { + continue + } + if !s.rescanFilter.ExistsAddress(addr) { continue } @@ -115,7 +120,11 @@ Txs: continue } for _, a := range addrs { - if s.rescanFilter.ExistsAddress(a) { + addr, err := payments.WrapUtilAddress(a) + if err != nil { + continue + } + if s.rescanFilter.ExistsAddress(addr) { matches = append(matches, tx) continue Txs } diff --git a/ticketbuyer/tb.go b/ticketbuyer/tb.go index 4c1cccc3d..8874aad34 100644 --- a/ticketbuyer/tb.go +++ b/ticketbuyer/tb.go @@ -11,6 +11,7 @@ import ( "sync" "decred.org/dcrwallet/errors" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/vsp" "decred.org/dcrwallet/wallet" "decred.org/dcrwallet/wallet/txrules" @@ -35,10 +36,10 @@ type Config struct { Maintain dcrutil.Amount // Address to assign voting rights; overrides VotingAccount - VotingAddr dcrutil.Address + VotingAddr payments.Address // Commitment address for stakepool fees - PoolFeeAddr dcrutil.Address + PoolFeeAddr payments.Address // Stakepool fee percentage (between 0-100) PoolFees float64 diff --git a/vsp/feeaddress.go b/vsp/feeaddress.go index d9836e1f4..3850605dc 100644 --- a/vsp/feeaddress.go +++ b/vsp/feeaddress.go @@ -12,10 +12,9 @@ import ( "net/http" "time" - "github.com/decred/dcrd/blockchain/stake/v3" + "decred.org/dcrwallet/payments" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/dcrutil/v3" - "github.com/decred/dcrd/txscript/v3" ) func (v *VSP) GetFeeAddress(ctx context.Context, ticketHash *chainhash.Hash) (dcrutil.Amount, error) { @@ -27,19 +26,15 @@ func (v *VSP) GetFeeAddress(ctx context.Context, ticketHash *chainhash.Hash) (dc ticketTx := txs[0] const scriptVersion = 0 - _, addrs, _, err := txscript.ExtractPkScriptAddrs(scriptVersion, - ticketTx.TxOut[0].PkScript, v.params) + pkScript := ticketTx.TxOut[0].PkScript + votingAddr, err := payments.ParseAddress(scriptVersion, pkScript, v.params) if err != nil { log.Errorf("failed to extract stake submission address from %v: %v", ticketHash, err) return 0, err } - if len(addrs) == 0 { - log.Errorf("failed to get address from %v", ticketHash) - return 0, fmt.Errorf("failed to get address from %v", ticketHash) - } - votingAddress := addrs[0] - commitmentAddr, err := stake.AddrFromSStxPkScrCommitment(ticketTx.TxOut[1].PkScript, v.params) + pkScript = ticketTx.TxOut[1].PkScript + commitmentAddr, err := payments.ParseTicketCommitmentAddress(pkScript, v.params) if err != nil { log.Errorf("failed to extract script addr from %v: %v", ticketHash, err) return 0, err @@ -133,7 +128,7 @@ func (v *VSP) GetFeeAddress(ctx context.Context, ticketHash *chainhash.Hash) (dc } // TODO - validate server timestamp? - feeAddress, err := dcrutil.DecodeAddress(feeResponse.FeeAddress, v.params) + feeAddr, err := payments.DecodeAddress(feeResponse.FeeAddress, v.params) if err != nil { log.Warnf("server fee address invalid: %v", err) return 0, fmt.Errorf("server fee address invalid: %v", err) @@ -154,8 +149,8 @@ func (v *VSP) GetFeeAddress(ctx context.Context, ticketHash *chainhash.Hash) (dc v.ticketToFeeMu.Lock() v.ticketToFeeMap[*ticketHash] = PendingFee{ CommitmentAddress: commitmentAddr, - VotingAddress: votingAddress, - FeeAddress: feeAddress, + VotingAddress: votingAddr, + FeeAddress: feeAddr, FeeAmount: feeAmount, } v.ticketToFeeMu.Unlock() diff --git a/vsp/payfee.go b/vsp/payfee.go index dac897e41..048da73f3 100644 --- a/vsp/payfee.go +++ b/vsp/payfee.go @@ -12,6 +12,7 @@ import ( "net/http" "time" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/wallet" "decred.org/dcrwallet/wallet/txauthor" "decred.org/dcrwallet/wallet/txsizes" @@ -46,22 +47,16 @@ func (v *VSP) PayFee(ctx context.Context, ticketHash *chainhash.Hash, credits [] return nil, fmt.Errorf("not enough fee: %v < %v", dcrutil.Amount(totalValue), feeInfo.FeeAmount) } - pkScript, err := txscript.PayToAddrScript(feeInfo.FeeAddress) - if err != nil { - log.Warnf("failed to generate pay to addr script for %v: %v", feeInfo.FeeAddress, err) - return nil, err - } - a, err := v.w.NewChangeAddress(ctx, v.changeAccount) if err != nil { log.Warnf("failed to get new change address: %v", err) return nil, err } - c, ok := a.(wallet.Address) + c, ok := a.(payments.Address) if !ok { - log.Warnf("failed to convert '%T' to wallet.Address", a) - return nil, fmt.Errorf("failed to convert '%T' to wallet.Address", a) + log.Warnf("failed to convert '%T' to payments.Address", a) + return nil, fmt.Errorf("failed to convert '%T' to payments.Address", a) } cver, cscript := c.PaymentScript() @@ -134,7 +129,10 @@ func (v *VSP) PayFee(ctx context.Context, ticketHash *chainhash.Hash, credits [] } } - txOut := []*wire.TxOut{wire.NewTxOut(int64(feeInfo.FeeAmount), pkScript)} + out := wire.NewTxOut(int64(feeInfo.FeeAmount), nil) + out.Version, out.PkScript = feeInfo.FeeAddress.PaymentScript() + + txOut := []*wire.TxOut{out} feeTx, err := v.w.NewUnsignedTransaction(ctx, txOut, v.w.RelayFee(), v.purchaseAccount, 6, wallet.OutputSelectionAlgorithmDefault, cs, inputSource) if err != nil { diff --git a/vsp/ticketstatus.go b/vsp/ticketstatus.go index 08ecbdb04..db8da32ee 100644 --- a/vsp/ticketstatus.go +++ b/vsp/ticketstatus.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "net/http" + "decred.org/dcrwallet/payments" "github.com/decred/dcrd/blockchain/stake/v3" "github.com/decred/dcrd/chaincfg/chainhash" ) @@ -28,7 +29,7 @@ func (v *VSP) TicketStatus(ctx context.Context, hash *chainhash.Hash) (*TicketSt log.Errorf("%v is not a ticket", hash) return nil, fmt.Errorf("%v is not a ticket", hash) } - commitmentAddr, err := stake.AddrFromSStxPkScrCommitment(ticketTx.TxOut[1].PkScript, v.params) + commitmentAddr, err := payments.ParseTicketCommitmentAddress(ticketTx.TxOut[1].PkScript, v.params) if err != nil { log.Errorf("failed to extract script addr from %v: %v", hash, err) return nil, err diff --git a/vsp/vsp.go b/vsp/vsp.go index f3188eaa4..10f091f33 100644 --- a/vsp/vsp.go +++ b/vsp/vsp.go @@ -9,6 +9,7 @@ import ( "net/http" "sync" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/wallet" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/chaincfg/v3" @@ -24,9 +25,9 @@ const ( ) type PendingFee struct { - CommitmentAddress dcrutil.Address - VotingAddress dcrutil.Address - FeeAddress dcrutil.Address + CommitmentAddress payments.Address + VotingAddress payments.Address + FeeAddress payments.Address FeeAmount dcrutil.Amount FeeTx *wire.MsgTx } diff --git a/wallet/addresses.go b/wallet/addresses.go index 36f96d606..3a3efc044 100644 --- a/wallet/addresses.go +++ b/wallet/addresses.go @@ -6,16 +6,14 @@ package wallet import ( "context" - "encoding/binary" "runtime/trace" "decred.org/dcrwallet/errors" - "decred.org/dcrwallet/internal/compat" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/wallet/txsizes" "decred.org/dcrwallet/wallet/udb" "decred.org/dcrwallet/wallet/walletdb" "github.com/decred/dcrd/chaincfg/v3" - "github.com/decred/dcrd/dcrec" "github.com/decred/dcrd/dcrutil/v3" "github.com/decred/dcrd/hdkeychain/v3" "github.com/decred/dcrd/txscript/v3" @@ -41,26 +39,10 @@ const ( AccountKindImportedXpub ) -// Address is a human-readable encoding of an output script. -// -// Address encodings may include a network identifier, to prevent misuse on an -// alternate Decred network. -type Address interface { - String() string - - // PaymentScript returns the output script and script version to pay the - // address. The version is always returned with the script, as it is - // not useful to use the script without the version. - PaymentScript() (version uint16, script []byte) - - // ScriptLen returns the known length of the address output script. - ScriptLen() int -} - -// KnownAddress represents an address recorded by the wallet. It is potentially +// Address represents a known address recorded by the wallet. It is potentially // watched for involving transactions during wallet syncs. -type KnownAddress interface { - Address +type Address interface { + payments.Address // AccountName returns the account name associated with the known // address. @@ -70,21 +52,26 @@ type KnownAddress interface { AccountKind() AccountKind } -// PubKeyHashAddress is a KnownAddress for a secp256k1 pay-to-pubkey-hash +// PubKeyHashAddress is a known address for a secp256k1 pay-to-pubkey-hash // (P2PKH) output script. type PubKeyHashAddress interface { - KnownAddress + Address + payments.PubKeyHashAddress + payments.PubKeyAddress +} - // PubKey returns the serialized compressed public key. This key must - // be included in scripts redeeming P2PKH outputs paying the address. - PubKey() []byte +// P2SHAddress is a known address which pays to the hash of an arbitrary script. +type P2SHAddress interface { + Address - // PubKeyHash returns the hashed compressed public key. This hash must - // appear in output scripts paying to the address. - PubKeyHash() []byte + // RedeemScript returns the preimage of the script hash. The returned + // version is the script version of the address, or the script version + // of the redeemed previous output, and must be used for any operations + // involving the script. + RedeemScript() (version uint16, script []byte) } -// BIP0044Address is a KnownAddress for a secp256k1 pay-to-pubkey-hash output, +// BIP0044Address is an Address for a secp256k1 pay-to-pubkey-hash output, // with keys created from a derived or imported BIP0044 account extended pubkey. type BIP0044Address interface { PubKeyHashAddress @@ -97,18 +84,7 @@ type BIP0044Address interface { Path() (account, branch, child uint32) } -// P2SHAddress is a KnownAddress which pays to the hash of an arbitrary script. -type P2SHAddress interface { - KnownAddress - - // RedeemScript returns the preimage of the script hash. The returned - // version is the script version of the address, or the script version - // of the redeemed previous output, and must be used for any operations - // involving the script. - RedeemScript() (version uint16, script []byte) -} - -// managedAddress implements KnownAddress for a wrapped udb.ManagedAddress. +// managedAddress implements Address for a wrapped udb.ManagedAddress. type managedAddress struct { acct string acctKind AccountKind @@ -117,6 +93,8 @@ type managedAddress struct { scriptLen int } +var _ payments.Address = (*managedAddress)(nil) + func (m *managedAddress) String() string { return m.addr.Address().String() } func (m *managedAddress) PaymentScript() (uint16, []byte) { return m.script() } func (m *managedAddress) ScriptLen() int { return m.scriptLen } @@ -152,6 +130,12 @@ type managedP2PKHAddress struct { managedAddress } +var _ interface { + payments.PubKeyHashAddress + Address + PubKeyHashAddress +} = (*managedP2PKHAddress)(nil) + func (m *managedP2PKHAddress) PubKey() []byte { return m.addr.(udb.ManagedPubKeyAddress).PubKey() } @@ -174,11 +158,21 @@ type managedP2SHAddress struct { managedAddress } +var _ interface { + payments.ScriptHashAddress + Address + P2SHAddress +} = (*managedP2SHAddress)(nil) + func (m *managedP2SHAddress) RedeemScript() (uint16, []byte) { return m.addr.(udb.ManagedScriptAddress).RedeemScript() } -func wrapManagedAddress(addr udb.ManagedAddress, account string, kind AccountKind) (KnownAddress, error) { +func (m *managedP2SHAddress) ScriptHash() []byte { + return m.addr.(udb.ManagedScriptAddress).AddrHash() +} + +func wrapManagedAddress(addr udb.ManagedAddress, account string, kind AccountKind) (Address, error) { ma := managedAddress{ acct: account, acctKind: kind, @@ -216,11 +210,11 @@ func wrapManagedAddress(addr udb.ManagedAddress, account string, kind AccountKin } } -// KnownAddress returns the KnownAddress implementation for an address. The +// KnownAddress returns the Address implementation for an address. The // returned address may implement other interfaces (such as, but not limited to, // PubKeyHashAddress, BIP0044Address, or P2SHAddress) depending on the script // type and account for the address. -func (w *Wallet) KnownAddress(ctx context.Context, a dcrutil.Address) (KnownAddress, error) { +func (w *Wallet) KnownAddress(ctx context.Context, a payments.Address) (Address, error) { const op errors.Op = "wallet.KnownAddress" var ma udb.ManagedAddress @@ -228,7 +222,10 @@ func (w *Wallet) KnownAddress(ctx context.Context, a dcrutil.Address) (KnownAddr var acctName string err := walletdb.View(ctx, w.db, func(tx walletdb.ReadTx) error { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) - var err error + a, err := payments.AddressToUtilAddress(a, w.chainParams) + if err != nil { + return err + } ma, err = w.manager.Address(addrmgrNs, a) if err != nil { return err @@ -253,48 +250,31 @@ func (w *Wallet) KnownAddress(ctx context.Context, a dcrutil.Address) (KnownAddr return wrapManagedAddress(ma, acctName, acctKind) } -type stakeAddress interface { - voteRights() (script []byte, version uint16) - ticketChange() (script []byte, version uint16) - rewardCommitment(amount dcrutil.Amount, limits uint16) (script []byte, version uint16) - payVoteCommitment() (script []byte, version uint16) - payRevokeCommitment() (script []byte, version uint16) -} - -type xpubAddress struct { - *dcrutil.AddressPubKeyHash - xpub *hdkeychain.ExtendedKey +type hdAddress struct { + payments.PubKeyHashAddress accountName string account uint32 branch uint32 child uint32 } -var _ BIP0044Address = (*xpubAddress)(nil) -var _ stakeAddress = (*xpubAddress)(nil) - -func (x *xpubAddress) PaymentScript() (uint16, []byte) { - s := []byte{ - 0: txscript.OP_DUP, - 1: txscript.OP_HASH160, - 2: txscript.OP_DATA_20, - 23: txscript.OP_EQUALVERIFY, - 24: txscript.OP_CHECKSIG, - } - copy(s[3:23], x.Hash160()[:]) - return 0, s +type xpubAddress struct { + hdAddress + xpub *hdkeychain.ExtendedKey } -func (x *xpubAddress) ScriptLen() int { return txsizes.P2PKHPkScriptSize } -func (x *xpubAddress) AccountName() string { return x.accountName } +var _ BIP0044Address = (*xpubAddress)(nil) -func (x *xpubAddress) AccountKind() AccountKind { +func (x *hdAddress) AccountName() string { return x.accountName } + +func (x *hdAddress) AccountKind() AccountKind { + // XXX if x.account > udb.ImportedAddrAccount { return AccountKindImportedXpub } return AccountKindBIP0044 } -func (x *xpubAddress) Path() (account, branch, child uint32) { +func (x *hdAddress) Path() (account, branch, child uint32) { account, branch, child = x.account, x.branch, x.child if x.account > udb.ImportedAddrAccount { account = 0 @@ -316,70 +296,6 @@ func (x *xpubAddress) PubKey() []byte { return childKey.SerializedPubKey() } -func (x *xpubAddress) PubKeyHash() []byte { return x.Hash160()[:] } - -func (x *xpubAddress) voteRights() (script []byte, version uint16) { - s := []byte{ - 0: txscript.OP_SSTX, - 1: txscript.OP_DUP, - 2: txscript.OP_HASH160, - 3: txscript.OP_DATA_20, - 24: txscript.OP_EQUALVERIFY, - 25: txscript.OP_CHECKSIG, - } - copy(s[4:24], x.PubKeyHash()) - return s, 0 -} - -func (x *xpubAddress) ticketChange() (script []byte, version uint16) { - s := []byte{ - 0: txscript.OP_SSTXCHANGE, - 1: txscript.OP_DUP, - 2: txscript.OP_HASH160, - 3: txscript.OP_DATA_20, - 24: txscript.OP_EQUALVERIFY, - 25: txscript.OP_CHECKSIG, - } - copy(s[4:24], x.PubKeyHash()) - return s, 0 -} - -func (x *xpubAddress) rewardCommitment(amount dcrutil.Amount, limits uint16) ([]byte, uint16) { - s := make([]byte, 32) - s[0] = txscript.OP_RETURN - s[1] = txscript.OP_DATA_30 - copy(s[2:22], x.PubKeyHash()) - binary.LittleEndian.PutUint64(s[22:30], uint64(amount)) - binary.LittleEndian.PutUint16(s[30:32], limits) - return s, 0 -} - -func (x *xpubAddress) payVoteCommitment() (script []byte, version uint16) { - s := []byte{ - 0: txscript.OP_SSGEN, - 1: txscript.OP_DUP, - 2: txscript.OP_HASH160, - 3: txscript.OP_DATA_20, - 24: txscript.OP_EQUALVERIFY, - 25: txscript.OP_CHECKSIG, - } - copy(s[4:24], x.PubKeyHash()) - return s, 0 -} - -func (x *xpubAddress) payRevokeCommitment() (script []byte, version uint16) { - s := []byte{ - 0: txscript.OP_SSRTX, - 1: txscript.OP_DUP, - 2: txscript.OP_HASH160, - 3: txscript.OP_DATA_20, - 24: txscript.OP_EQUALVERIFY, - 25: txscript.OP_CHECKSIG, - } - copy(s[4:24], x.PubKeyHash()) - return s, 0 -} - // addressScript returns an output script paying to address. This func is // always preferred over direct usage of txscript.PayToAddrScript due to the // latter failing on unexpected concrete types. @@ -399,8 +315,8 @@ func addressScript(addr dcrutil.Address) (pkScript []byte, version uint16, err e func voteRightsScript(addr dcrutil.Address) (script []byte, version uint16, err error) { switch addr := addr.(type) { - case stakeAddress: - script, version = addr.voteRights() + case payments.StakeAddress: + script, version = addr.VoteRights() default: script, err = txscript.PayToSStx(addr) } @@ -409,8 +325,8 @@ func voteRightsScript(addr dcrutil.Address) (script []byte, version uint16, err func ticketChangeScript(addr dcrutil.Address) (script []byte, version uint16, err error) { switch addr := addr.(type) { - case stakeAddress: - script, version = addr.ticketChange() + case payments.StakeAddress: + script, version = addr.TicketChange() default: script, err = txscript.PayToSStxChange(addr) } @@ -419,8 +335,8 @@ func ticketChangeScript(addr dcrutil.Address) (script []byte, version uint16, er func rewardCommitment(addr dcrutil.Address, amount dcrutil.Amount, limits uint16) (script []byte, version uint16, err error) { switch addr := addr.(type) { - case stakeAddress: - script, version = addr.rewardCommitment(amount, limits) + case payments.StakeAddress: + script, version = addr.RewardCommitment(amount, limits) default: script, err = txscript.GenerateSStxAddrPush(addr, amount, limits) } @@ -429,8 +345,8 @@ func rewardCommitment(addr dcrutil.Address, amount dcrutil.Amount, limits uint16 func payVoteCommitment(addr dcrutil.Address) (script []byte, version uint16, err error) { switch addr := addr.(type) { - case stakeAddress: - script, version = addr.payVoteCommitment() + case payments.StakeAddress: + script, version = addr.PayVoteCommitment() default: script, err = txscript.PayToSSGen(addr) } @@ -439,8 +355,8 @@ func payVoteCommitment(addr dcrutil.Address) (script []byte, version uint16, err func payRevokeCommitment(addr dcrutil.Address) (script []byte, version uint16, err error) { switch addr := addr.(type) { - case stakeAddress: - script, version = addr.payRevokeCommitment() + case payments.StakeAddress: + script, version = addr.PayRevokeCommitment() default: script, err = txscript.PayToSSRtx(addr) } @@ -571,35 +487,10 @@ func (w *Wallet) persistReturnedChild(ctx context.Context, maybeDBTX walletdb.Re } } -// deferPersistReturnedChild returns a persistReturnedChildFunc that is not -// immediately written to the database. Instead, an update function is appended -// to the updates slice. This allows all updates to be run under a single -// database update later and allows deferred child persistence even when -// generating addresess in a view (as long as the update is called after). -// -// This is preferable to running updates asynchronously using goroutines as it -// allows the updates to not be performed if a later error occurs and the child -// indexes should not be written. It also allows the updates to be grouped -// together in a single atomic transaction. -func (w *Wallet) deferPersistReturnedChild(ctx context.Context, updates *[]func(walletdb.ReadWriteTx) error) persistReturnedChildFunc { - // These vars are closed-over by the update function and modified by the - // returned persist function. - var account, branch, child uint32 - update := func(tx walletdb.ReadWriteTx) error { - persist := w.persistReturnedChild(ctx, tx) - return persist(account, branch, child) - } - *updates = append(*updates, update) - return func(a, b, c uint32) error { - account, branch, child = a, b, c - return nil - } -} - // nextAddress returns the next address of an account branch. -func (w *Wallet) nextAddress(ctx context.Context, op errors.Op, persist persistReturnedChildFunc, - accountName string, account, branch uint32, - callOpts ...NextAddressCallOption) (dcrutil.Address, error) { +func (w *Wallet) nextAddress(ctx context.Context, op errors.Op, dbtx walletdb.ReadWriteTx, + persist persistReturnedChildFunc, accountName string, account, branch uint32, + callOpts ...NextAddressCallOption) (Address, error) { var opts nextAddressCallOptions // TODO: zero values for now, add to wallet config later. for _, c := range callOpts { @@ -674,7 +565,8 @@ func (w *Wallet) nextAddress(ctx context.Context, op errors.Op, persist persistR if err != nil { return nil, errors.E(op, err) } - apkh, err := compat.HD2Address(child, w.chainParams) + pkh := dcrutil.Hash160(child.SerializedPubKey()) + pkha, err := payments.P2PKHAddress(pkh, w.chainParams) if err != nil { return nil, errors.E(op, err) } @@ -684,14 +576,13 @@ func (w *Wallet) nextAddress(ctx context.Context, op errors.Op, persist persistR return nil, errors.E(op, err) } alb.cursor++ - addr := &xpubAddress{ - AddressPubKeyHash: apkh, - xpub: ad.xpub, - account: account, - accountName: accountName, - branch: branch, - child: childIndex, - } + addr := new(xpubAddress) + addr.PubKeyHashAddress = pkha + addr.account = account + addr.accountName = accountName + addr.branch = branch + addr.child = childIndex + addr.xpub = ad.xpub log.Infof("Returning address (account=%v branch=%v child=%v)", account, branch, childIndex) return addr, nil } @@ -699,7 +590,7 @@ func (w *Wallet) nextAddress(ctx context.Context, op errors.Op, persist persistR func (w *Wallet) nextImportedXpubAddress(ctx context.Context, op errors.Op, maybeDBTX walletdb.ReadWriteTx, accountName string, account uint32, branch uint32, - callOpts ...NextAddressCallOption) (addr dcrutil.Address, err error) { + callOpts ...NextAddressCallOption) (_ Address, err error) { dbtx := maybeDBTX if dbtx == nil { @@ -748,18 +639,17 @@ func (w *Wallet) nextImportedXpubAddress(ctx context.Context, op errors.Op, break } pkh := dcrutil.Hash160(childKey.SerializedPubKey()) - apkh, err := dcrutil.NewAddressPubKeyHash(pkh, w.chainParams, - dcrec.STEcdsaSecp256k1) + pkha, err := payments.P2PKHAddress(pkh, w.chainParams) if err != nil { return nil, errors.E(op, err) } - addr = &xpubAddress{ - AddressPubKeyHash: apkh, - xpub: xpub, - account: account, - branch: branch, - child: child, - } + addr := new(xpubAddress) + addr.PubKeyHashAddress = pkha + addr.account = account + addr.accountName = accountName + addr.branch = branch + addr.child = child + addr.xpub = xpub err = w.manager.MarkReturnedChildIndex(dbtx, account, branch, child) if err != nil { return nil, errors.E(op, err) @@ -806,26 +696,34 @@ func (w *Wallet) markUsedAddress(op errors.Op, dbtx walletdb.ReadWriteTx, addr u return nil } -// NewExternalAddress returns an external address. -func (w *Wallet) NewExternalAddress(ctx context.Context, account uint32, callOpts ...NextAddressCallOption) (dcrutil.Address, error) { - const op errors.Op = "wallet.NewExternalAddress" +func (w *Wallet) newAddressOpenDBTX(ctx context.Context, op errors.Op, account, branch uint32, + callOpts ...NextAddressCallOption) (addr Address, err error) { accountName, _ := w.AccountName(ctx, account) - return w.nextAddress(ctx, op, w.persistReturnedChild(ctx, nil), - accountName, account, udb.ExternalBranch, callOpts...) + err = walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { + var err error + addr, err = w.nextAddress(ctx, op, dbtx, w.persistReturnedChild(ctx, dbtx), + accountName, account, branch, callOpts...) + return err + }) + return addr, err } -// NewInternalAddress returns an internal address. -func (w *Wallet) NewInternalAddress(ctx context.Context, account uint32, callOpts ...NextAddressCallOption) (dcrutil.Address, error) { +// NewExternalAddress returns an external address. +func (w *Wallet) NewExternalAddress(ctx context.Context, account uint32, callOpts ...NextAddressCallOption) (addr Address, err error) { const op errors.Op = "wallet.NewExternalAddress" + return w.newAddressOpenDBTX(ctx, op, account, 0, callOpts...) +} - accountName, _ := w.AccountName(ctx, account) - return w.nextAddress(ctx, op, w.persistReturnedChild(ctx, nil), - accountName, account, udb.InternalBranch, callOpts...) +// NewInternalAddress returns an internal address. +func (w *Wallet) NewInternalAddress(ctx context.Context, account uint32, callOpts ...NextAddressCallOption) (Address, error) { + const op errors.Op = "wallet.NewInternalAddress" + return w.newAddressOpenDBTX(ctx, op, account, 1, callOpts...) } -func (w *Wallet) newChangeAddress(ctx context.Context, op errors.Op, persist persistReturnedChildFunc, - accountName string, account uint32, gap gapPolicy) (dcrutil.Address, error) { +func (w *Wallet) newChangeAddress(ctx context.Context, op errors.Op, dbtx walletdb.ReadWriteTx, + persist persistReturnedChildFunc, accountName string, account uint32, + gap gapPolicy) (Address, error) { // Addresses can not be generated for the imported account, so as a // workaround, change is sent to the first account. // @@ -833,17 +731,23 @@ func (w *Wallet) newChangeAddress(ctx context.Context, op errors.Op, persist per if account == udb.ImportedAddrAccount { account = udb.DefaultAccountNum } - return w.nextAddress(ctx, op, persist, accountName, account, udb.InternalBranch, withGapPolicy(gap)) + return w.nextAddress(ctx, op, dbtx, persist, accountName, account, 1, withGapPolicy(gap)) } // NewChangeAddress returns an internal address. This is identical to // NewInternalAddress but handles the imported account (which can't create // addresses) by using account 0 instead, and always uses the wrapping gap limit // policy. -func (w *Wallet) NewChangeAddress(ctx context.Context, account uint32) (dcrutil.Address, error) { +func (w *Wallet) NewChangeAddress(ctx context.Context, account uint32) (addr Address, err error) { const op errors.Op = "wallet.NewChangeAddress" accountName, _ := w.AccountName(ctx, account) - return w.newChangeAddress(ctx, op, w.persistReturnedChild(ctx, nil), accountName, account, gapPolicyWrap) + err = walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { + var err error + addr, err = w.newChangeAddress(ctx, op, dbtx, + w.persistReturnedChild(ctx, dbtx), accountName, account, gapPolicyWrap) + return err + }) + return addr, err } // BIP0044BranchNextIndexes returns the next external and internal branch child @@ -851,13 +755,28 @@ func (w *Wallet) NewChangeAddress(ctx context.Context, account uint32) (dcrutil. func (w *Wallet) BIP0044BranchNextIndexes(ctx context.Context, account uint32) (extChild, intChild uint32, err error) { const op errors.Op = "wallet.BIP0044BranchNextIndexes" - defer w.addressBuffersMu.Unlock() w.addressBuffersMu.Lock() - acctData, ok := w.addressBuffers[account] if !ok { + w.addressBuffersMu.Unlock() + + err = walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { + ns := dbtx.ReadBucket(waddrmgrNamespaceKey) + props, err := w.manager.AccountProperties(ns, account) + if err != nil { + return err + } + extChild = props.LastReturnedExternalIndex + 1 + intChild = props.LastReturnedInternalIndex + 1 + return nil + }) + if err != nil { + return + } + return 0, 0, errors.E(op, errors.NotExist, errors.Errorf("account %v", account)) } + defer w.addressBuffersMu.Unlock() extChild = acctData.albExternal.lastUsed + 1 + acctData.albExternal.cursor intChild = acctData.albInternal.lastUsed + 1 + acctData.albInternal.cursor return extChild, intChild, nil @@ -866,69 +785,86 @@ func (w *Wallet) BIP0044BranchNextIndexes(ctx context.Context, account uint32) ( // SyncLastReturnedAddress advances the last returned child address for a // BIP00044 account branch. The next returned address for the branch will be // child+1. -func (w *Wallet) SyncLastReturnedAddress(ctx context.Context, account, branch, child uint32) error { - const op errors.Op = "wallet.ExtendWatchedAddresses" - - var ( - branchXpub *hdkeychain.ExtendedKey - lastUsed uint32 - ) - err := func() error { - defer w.addressBuffersMu.Unlock() - w.addressBuffersMu.Lock() - - acctData, ok := w.addressBuffers[account] - if !ok { - return errors.E(op, errors.NotExist, errors.Errorf("account %v", account)) +func (w *Wallet) SyncLastReturnedAddress(ctx context.Context, account, branch, child uint32) (err error) { + defer func() { + const op errors.Op = "wallet.SyncLastReturnedAddress" + if err != nil { + err = errors.E(op, err) } - var alb *addressBuffer - switch branch { - case udb.ExternalBranch: - alb = &acctData.albInternal - case udb.InternalBranch: - alb = &acctData.albInternal - default: - return errors.E(op, errors.Invalid, "branch must be external (0) or internal (1)") + }() + + var addrs []payments.Address + + err = walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { + ns := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + props, err := w.manager.AccountProperties(ns, account) + if err != nil { + return err } - branchXpub = alb.branchXpub - lastUsed = alb.lastUsed - if lastUsed != ^uint32(0) && child > lastUsed { - alb.cursor = child - lastUsed + lastReturned := props.LastReturnedExternalIndex + if branch == 1 { + lastReturned = props.LastReturnedInternalIndex + } + if lastReturned != ^uint32(0) && child <= lastReturned { + // Nothing to derive + return nil } - return nil - }() - if err != nil { - return err - } - err = walletdb.Update(ctx, w.db, func(tx walletdb.ReadWriteTx) error { - ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) - err = w.manager.SyncAccountToAddrIndex(ns, account, child, branch) + getAccountKey := w.manager.AccountExtendedPubKey + + // Note: account extended privkeys are intentionally not cleared + // here. This is due to the key being cached and managed by the + // address manager, and zeroing would neuter the private key and + // break other functionality. When this changes and udb becomes + // a dumb storage layer, the key should be zeroed here, or + // cached by the wallet structure. + acctKey, err := getAccountKey(dbtx, account) + if err != nil { + return err + } + branchKey, err := acctKey.Child(branch) if err != nil { return err } - return w.manager.MarkReturnedChildIndex(tx, account, branch, child) + defer branchKey.Zero() + + // XXX record additional addresses in the gap limit? + // ideally syncers would look up relevance via the wallet + // rather than their own memory structures, leaving us with recording + // these addresses in the db, or saving the gap limit addresses + // in memory before finally saving them to the db. + for i := lastReturned + 1; i <= child; i++ { + childKey, err := branchKey.Child(i) + if err != nil { + if errors.Is(err, hdkeychain.ErrInvalidChild) { + continue + } + return err + } + pubkey := childKey.SerializedPubKey() + childKey.Zero() + + err = w.manager.RecordDerivedAddress(dbtx, account, branch, child, pubkey) + if err != nil { + return err + } + + pubkeyHash := dcrutil.Hash160(pubkey) + addr, err := payments.P2PKHAddress(pubkeyHash, w.chainParams) + addrs = append(addrs, addr) + } + + return w.manager.MarkReturnedChildIndex(dbtx, account, branch, child) }) if err != nil { return err } if n, err := w.NetworkBackend(); err == nil { - lastWatched := lastUsed + w.gapLimit - if child <= lastWatched { - // No need to derive anything more. - return nil - } - additionalAddrs := child - lastWatched - addrs, err := deriveChildAddresses(branchXpub, lastUsed+1+w.gapLimit, - additionalAddrs, w.chainParams) - if err != nil { - return errors.E(op, err) - } err = n.LoadTxFilter(ctx, false, addrs, nil) if err != nil { - return errors.E(op, err) + return err } } @@ -936,7 +872,7 @@ func (w *Wallet) SyncLastReturnedAddress(ctx context.Context, account, branch, c } // ImportedAddresses returns each of the addresses imported into an account. -func (w *Wallet) ImportedAddresses(ctx context.Context, account string) (_ []KnownAddress, err error) { +func (w *Wallet) ImportedAddresses(ctx context.Context, account string) (_ []Address, err error) { const opf = "wallet.ImportedAddresses(%q)" defer func() { if err != nil { @@ -949,7 +885,7 @@ func (w *Wallet) ImportedAddresses(ctx context.Context, account string) (_ []Kno return nil, errors.E("account does not record imported keys") } - var addrs []KnownAddress + var addrs []Address err = walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { ns := dbtx.ReadBucket(waddrmgrNamespaceKey) f := func(a udb.ManagedAddress) error { @@ -967,6 +903,7 @@ func (w *Wallet) ImportedAddresses(ctx context.Context, account string) (_ []Kno type p2PKHChangeSource struct { persist persistReturnedChildFunc + dbtx walletdb.ReadWriteTx account uint32 wallet *Wallet ctx context.Context @@ -975,20 +912,22 @@ type p2PKHChangeSource struct { func (src *p2PKHChangeSource) Script() ([]byte, uint16, error) { const accountName = "" // not returned, so can be faked. - changeAddress, err := src.wallet.newChangeAddress(src.ctx, "", src.persist, - accountName, src.account, src.gapPolicy) + changeAddress, err := src.wallet.newChangeAddress(src.ctx, "", src.dbtx, + src.persist, accountName, src.account, src.gapPolicy) if err != nil { return nil, 0, err } - return addressScript(changeAddress) + version, script := changeAddress.PaymentScript() + return script, version, nil } func (src *p2PKHChangeSource) ScriptSize() int { return txsizes.P2PKHPkScriptSize } -func deriveChildAddresses(key *hdkeychain.ExtendedKey, startIndex, count uint32, params *chaincfg.Params) ([]dcrutil.Address, error) { - addresses := make([]dcrutil.Address, 0, count) +func deriveChildAddresses(key *hdkeychain.ExtendedKey, startIndex, count uint32, + params *chaincfg.Params) ([]payments.Address, error) { + addresses := make([]payments.Address, 0, count) for i := uint32(0); i < count; i++ { child, err := key.Child(startIndex + i) if errors.Is(err, hdkeychain.ErrInvalidChild) { @@ -997,7 +936,8 @@ func deriveChildAddresses(key *hdkeychain.ExtendedKey, startIndex, count uint32, if err != nil { return nil, err } - addr, err := compat.HD2Address(child, params) + pkh := dcrutil.Hash160(child.SerializedPubKey()) + addr, err := payments.P2PKHAddress(pkh, params) if err != nil { return nil, err } @@ -1006,12 +946,14 @@ func deriveChildAddresses(key *hdkeychain.ExtendedKey, startIndex, count uint32, return addresses, nil } -func deriveChildAddress(key *hdkeychain.ExtendedKey, child uint32, params *chaincfg.Params) (dcrutil.Address, error) { +func deriveChildAddress(key *hdkeychain.ExtendedKey, child uint32, + params *chaincfg.Params) (payments.Address, error) { childKey, err := key.Child(child) if err != nil { return nil, err } - return compat.HD2Address(childKey, params) + pkh := dcrutil.Hash160(childKey.SerializedPubKey()) + return payments.P2PKHAddress(pkh, params) } func deriveBranches(acctXpub *hdkeychain.ExtendedKey) (extKey, intKey *hdkeychain.ExtendedKey, err error) { diff --git a/wallet/addresses_test.go b/wallet/addresses_test.go index 0bd0e4215..2216bedf4 100644 --- a/wallet/addresses_test.go +++ b/wallet/addresses_test.go @@ -12,6 +12,7 @@ import ( "os" "testing" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/wallet/walletdb" "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/dcrutil/v3" @@ -165,7 +166,7 @@ func setupWallet(t *testing.T, cfg *Config) (*Wallet, walletdb.DB, func()) { return w, db, teardown } -type newAddressFunc func(*Wallet, context.Context, uint32, ...NextAddressCallOption) (dcrutil.Address, error) +type newAddressFunc func(*Wallet, context.Context, uint32, ...NextAddressCallOption) (Address, error) func testKnownAddresses(tc *testContext, prefix string, unlock bool, newAddr newAddressFunc, tests []expectedAddr) { w, db, teardown := setupWallet(tc.t, &walletConfig) @@ -341,9 +342,13 @@ func useAddress(child uint32) func(t *testing.T, w *Wallet) { if err != nil { t.Fatal(err) } + utilAddr, err := payments.AddressToUtilAddress(addr, basicWalletConfig.Params) + if err != nil { + t.Fatal(err) + } err = walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { ns := dbtx.ReadWriteBucket(waddrmgrBucketKey) - ma, err := w.manager.Address(ns, addr) + ma, err := w.manager.Address(ns, utilAddr) if err != nil { return err } diff --git a/wallet/chainntfns.go b/wallet/chainntfns.go index e08284689..f4fec3f0e 100644 --- a/wallet/chainntfns.go +++ b/wallet/chainntfns.go @@ -11,6 +11,7 @@ import ( "time" "decred.org/dcrwallet/errors" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/wallet/txrules" "decred.org/dcrwallet/wallet/udb" "decred.org/dcrwallet/wallet/walletdb" @@ -602,8 +603,12 @@ func (w *Wallet) processTransactionRecord(ctx context.Context, dbtx walletdb.Rea case err != nil: return nil, errors.E(op, err) case n != nil: - addrs := []dcrutil.Address{addr.Address()} - err := n.LoadTxFilter(ctx, false, addrs, nil) + addr, err := payments.WrapUtilAddress(addr.Address()) + if err != nil { + return nil, errors.E(op, err) + } + addrs := []payments.Address{addr} + err = n.LoadTxFilter(ctx, false, addrs, nil) if err != nil { return nil, errors.E(op, err) } diff --git a/wallet/coinjoin.go b/wallet/coinjoin.go index ac1ed374b..590648f1f 100644 --- a/wallet/coinjoin.go +++ b/wallet/coinjoin.go @@ -6,6 +6,7 @@ import ( "crypto/subtle" "decred.org/dcrwallet/errors" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/wallet/walletdb" "github.com/decred/dcrd/dcrec" "github.com/decred/dcrd/dcrutil/v3" @@ -64,30 +65,24 @@ func (c *csppJoin) Gen() ([][]byte, error) { const op errors.Op = "cspp.Gen" gen := make([][]byte, c.mcount) c.genScripts = make([][]byte, c.mcount) - var updates []func(walletdb.ReadWriteTx) error - for i := 0; i < c.mcount; i++ { - persist := c.wallet.deferPersistReturnedChild(c.ctx, &updates) - const accountName = "" // not used, so can be faked. - mixAddr, err := c.wallet.nextAddress(c.ctx, op, persist, - accountName, c.mixAccount, c.mixBranch, WithGapPolicyIgnore()) - if err != nil { - return nil, err - } - script, version, err := addressScript(mixAddr) - if err != nil { - return nil, err - } - if version != 0 { - return nil, errors.E("expected script version 0") - } - c.genScripts[i] = script - gen[i] = mixAddr.Hash160()[:] - } err := walletdb.Update(c.ctx, c.wallet.db, func(dbtx walletdb.ReadWriteTx) error { - for _, f := range updates { - if err := f(dbtx); err != nil { + for i := 0; i < c.mcount; i++ { + persist := c.wallet.persistReturnedChild(c.ctx, dbtx) + const accountName = "" // not used, so can be faked. + mixAddr, err := c.wallet.nextAddress(c.ctx, op, dbtx, persist, + accountName, c.mixAccount, c.mixBranch, WithGapPolicyIgnore()) + if err != nil { return err } + version, script := mixAddr.PaymentScript() + if err != nil { + return err + } + if version != 0 { + return errors.E("expected script version 0") + } + c.genScripts[i] = script + gen[i] = mixAddr.(payments.PubKeyHashAddress).PubKeyHash() } return nil }) diff --git a/wallet/createtx.go b/wallet/createtx.go index 8a4ba3dbe..8eedcc3a6 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -16,6 +16,7 @@ import ( "decred.org/cspp" "decred.org/cspp/coinjoin" "decred.org/dcrwallet/errors" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/wallet/txauthor" "decred.org/dcrwallet/wallet/txrules" "decred.org/dcrwallet/wallet/txsizes" @@ -98,8 +99,7 @@ func (w *Wallet) NewUnsignedTransaction(ctx context.Context, outputs []*wire.TxO w.lockedOutpointMu.Lock() var authoredTx *txauthor.AuthoredTx - var changeSourceUpdates []func(walletdb.ReadWriteTx) error - err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { + err := walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) _, tipHeight := w.txStore.MainChainTip(dbtx) @@ -138,7 +138,8 @@ func (w *Wallet) NewUnsignedTransaction(ctx context.Context, outputs []*wire.TxO if changeSource == nil { changeSource = &p2PKHChangeSource{ - persist: w.deferPersistReturnedChild(ctx, &changeSourceUpdates), + persist: w.persistReturnedChild(ctx, dbtx), + dbtx: dbtx, account: account, wallet: w, ctx: context.Background(), @@ -157,20 +158,6 @@ func (w *Wallet) NewUnsignedTransaction(ctx context.Context, outputs []*wire.TxO if err != nil { return nil, errors.E(op, err) } - if len(changeSourceUpdates) != 0 { - err := walletdb.Update(ctx, w.db, func(tx walletdb.ReadWriteTx) error { - for _, up := range changeSourceUpdates { - err := up(tx) - if err != nil { - return err - } - } - return nil - }) - if err != nil { - return nil, errors.E(op, err) - } - } return authoredTx, nil } @@ -351,8 +338,7 @@ func (w *Wallet) txToOutputs(ctx context.Context, op errors.Op, outputs []*wire. w.lockedOutpointMu.Lock() var atx *txauthor.AuthoredTx - var changeSourceUpdates []func(walletdb.ReadWriteTx) error - err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { + err := walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) @@ -361,7 +347,8 @@ func (w *Wallet) txToOutputs(ctx context.Context, op errors.Op, outputs []*wire. inputSource := w.txStore.MakeInputSource(txmgrNs, addrmgrNs, account, minconf, tipHeight, ignoreInput) changeSource := &p2PKHChangeSource{ - persist: w.deferPersistReturnedChild(ctx, &changeSourceUpdates), + persist: w.persistReturnedChild(ctx, dbtx), + dbtx: dbtx, account: changeAccount, wallet: w, ctx: ctx, @@ -433,13 +420,6 @@ func (w *Wallet) txToOutputs(ctx context.Context, op errors.Op, outputs []*wire. // before publishing the transaction to the network. var watch []wire.OutPoint err = walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { - for _, up := range changeSourceUpdates { - err := up(dbtx) - if err != nil { - return err - } - } - // TODO: this can be improved by not using the same codepath as notified // relevant transactions, since this does a lot of extra work. var err error @@ -456,13 +436,13 @@ func (w *Wallet) txToOutputs(ctx context.Context, op errors.Op, outputs []*wire. // txToMultisig spends funds to a multisig output, partially signs the // transaction, then returns fund func (w *Wallet) txToMultisig(ctx context.Context, op errors.Op, account uint32, amount dcrutil.Amount, pubkeys []*dcrutil.AddressSecpPubKey, - nRequired int8, minconf int32) (*CreatedTx, dcrutil.Address, []byte, error) { + nRequired int8, minconf int32) (*CreatedTx, payments.Address, []byte, error) { defer w.lockedOutpointMu.Unlock() w.lockedOutpointMu.Lock() var created *CreatedTx - var addr dcrutil.Address + var addr payments.Address var msScript []byte err := walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { var err error @@ -476,12 +456,13 @@ func (w *Wallet) txToMultisig(ctx context.Context, op errors.Op, account uint32, return created, addr, msScript, nil } -func (w *Wallet) txToMultisigInternal(ctx context.Context, op errors.Op, dbtx walletdb.ReadWriteTx, account uint32, amount dcrutil.Amount, - pubkeys []*dcrutil.AddressSecpPubKey, nRequired int8, minconf int32) (*CreatedTx, dcrutil.Address, []byte, error) { +func (w *Wallet) txToMultisigInternal(ctx context.Context, op errors.Op, dbtx walletdb.ReadWriteTx, + account uint32, amount dcrutil.Amount, pubkeys []*dcrutil.AddressSecpPubKey, nRequired int8, + minconf int32) (*CreatedTx, payments.Address, []byte, error) { addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) - txToMultisigError := func(err error) (*CreatedTx, dcrutil.Address, []byte, error) { + txToMultisigError := func(err error) (*CreatedTx, payments.Address, []byte, error) { return nil, nil, nil, err } @@ -556,14 +537,12 @@ func (w *Wallet) txToMultisigInternal(ctx context.Context, op errors.Op, dbtx wa return txToMultisigError(errors.E(op, err)) } } - scAddr, err := dcrutil.NewAddressScriptHash(msScript, w.chainParams) - if err != nil { - return txToMultisigError(errors.E(op, err)) - } - p2shScript, vers, err := addressScript(scAddr) + scriptHash := dcrutil.Hash160(msScript) + p2shAddr, err := payments.P2SHAddress(scriptHash, w.chainParams) if err != nil { return txToMultisigError(errors.E(op, err)) } + vers, p2shScript := p2shAddr.PaymentScript() txOut := &wire.TxOut{ Value: int64(amount), PkScript: p2shScript, @@ -584,6 +563,7 @@ func (w *Wallet) txToMultisigInternal(ctx context.Context, op errors.Op, dbtx wa if totalInput > amount+feeEst { changeSource := p2PKHChangeSource{ persist: w.persistReturnedChild(ctx, dbtx), + dbtx: dbtx, account: account, wallet: w, ctx: ctx, @@ -614,7 +594,7 @@ func (w *Wallet) txToMultisigInternal(ctx context.Context, op errors.Op, dbtx wa // Request updates from dcrd for new transactions sent to this // script hash address. - err = n.LoadTxFilter(ctx, false, []dcrutil.Address{scAddr}, nil) + err = n.LoadTxFilter(ctx, false, []payments.Address{p2shAddr}, nil) if err != nil { return txToMultisigError(errors.E(op, err)) } @@ -630,7 +610,8 @@ func (w *Wallet) txToMultisigInternal(ctx context.Context, op errors.Op, dbtx wa ChangeIndex: -1, } - return created, scAddr, msScript, nil + // XXX return (or even take) a payments.MultisigAddress + return created, p2shAddr, msScript, nil } // validateMsgTx verifies transaction input scripts for tx. All previous output @@ -666,7 +647,8 @@ func creditScripts(credits []Input) [][]byte { // compressWallet compresses all the utxos in a wallet into a single change // address. For use when it becomes dusty. -func (w *Wallet) compressWallet(ctx context.Context, op errors.Op, maxNumIns int, account uint32, changeAddr dcrutil.Address) (*chainhash.Hash, error) { +func (w *Wallet) compressWallet(ctx context.Context, op errors.Op, maxNumIns int, + account uint32, changeAddr payments.Address) (*chainhash.Hash, error) { defer w.lockedOutpointMu.Unlock() w.lockedOutpointMu.Lock() @@ -682,8 +664,8 @@ func (w *Wallet) compressWallet(ctx context.Context, op errors.Op, maxNumIns int return hash, nil } -func (w *Wallet) compressWalletInternal(ctx context.Context, op errors.Op, dbtx walletdb.ReadWriteTx, maxNumIns int, account uint32, - changeAddr dcrutil.Address) (*chainhash.Hash, error) { +func (w *Wallet) compressWalletInternal(ctx context.Context, op errors.Op, dbtx walletdb.ReadWriteTx, + maxNumIns int, account uint32, changeAddr payments.Address) (*chainhash.Hash, error) { addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) @@ -718,16 +700,13 @@ func (w *Wallet) compressWalletInternal(ctx context.Context, op errors.Op, dbtx // Check if output address is default, and generate a new address if needed if changeAddr == nil { const accountName = "" // not used, so can be faked. - changeAddr, err = w.newChangeAddress(ctx, op, w.persistReturnedChild(ctx, dbtx), + changeAddr, err = w.newChangeAddress(ctx, op, dbtx, w.persistReturnedChild(ctx, dbtx), accountName, account, gapPolicyIgnore) if err != nil { return nil, errors.E(op, err) } } - pkScript, vers, err := addressScript(changeAddr) - if err != nil { - return nil, errors.E(op, errors.Bug, err) - } + vers, pkScript := changeAddr.PaymentScript() msgtx := wire.NewMsgTx() msgtx.AddTxOut(&wire.TxOut{ Value: 0, @@ -806,8 +785,8 @@ func (w *Wallet) compressWalletInternal(ctx context.Context, op errors.Op, dbtx // makeTicket creates a ticket from a split transaction output. It can optionally // create a ticket that pays a fee to a pool if a pool input and pool address are // passed. -func makeTicket(params *chaincfg.Params, inputPool *Input, input *Input, addrVote dcrutil.Address, - addrSubsidy dcrutil.Address, ticketCost int64, addrPool dcrutil.Address) (*wire.MsgTx, error) { +func makeTicket(params *chaincfg.Params, inputPool *Input, input *Input, + addrVote, addrSubsidy, addrPool payments.StakeAddress, ticketCost int64) (*wire.MsgTx, error) { mtx := wire.NewMsgTx() @@ -824,10 +803,7 @@ func makeTicket(params *chaincfg.Params, inputPool *Input, input *Input, addrVot if addrVote == nil { return nil, errors.E(errors.Invalid, "nil vote address") } - pkScript, vers, err := voteRightsScript(addrVote) - if err != nil { - return nil, errors.E(errors.Invalid, errors.Errorf("vote address %v", addrVote)) - } + pkScript, vers := addrVote.VoteRights() txOut := &wire.TxOut{ Value: ticketCost, @@ -839,6 +815,7 @@ func makeTicket(params *chaincfg.Params, inputPool *Input, input *Input, addrVot // Obtain the commitment amounts. var amountsCommitted []int64 userSubsidyNullIdx := 0 + var err error if addrPool == nil { _, amountsCommitted, err = stake.SStxNullOutputAmounts( []int64{input.PrevOut.Value}, []int64{0}, ticketCost) @@ -866,12 +843,7 @@ func makeTicket(params *chaincfg.Params, inputPool *Input, input *Input, addrVot // commitment to the pool. limits := uint16(defaultTicketFeeLimits) if addrPool != nil { - pkScript, vers, err := rewardCommitment(addrPool, - dcrutil.Amount(amountsCommitted[0]), limits) - if err != nil { - return nil, errors.E(errors.Invalid, - errors.Errorf("pool commitment address %v", addrPool)) - } + pkScript, vers := addrPool.RewardCommitment(dcrutil.Amount(amountsCommitted[0]), limits) txout := &wire.TxOut{ Value: 0, PkScript: pkScript, @@ -900,12 +872,8 @@ func makeTicket(params *chaincfg.Params, inputPool *Input, input *Input, addrVot // Create an OP_RETURN push containing the pubkeyhash to send rewards to. // Apply limits to revocations for fees while not allowing // fees for votes. - pkScript, vers, err = rewardCommitment(addrSubsidy, + pkScript, vers = addrSubsidy.RewardCommitment( dcrutil.Amount(amountsCommitted[userSubsidyNullIdx]), limits) - if err != nil { - return nil, errors.E(errors.Invalid, - errors.Errorf("commitment address %v", addrSubsidy)) - } txout := &wire.TxOut{ Value: 0, PkScript: pkScript, @@ -977,7 +945,7 @@ func (w *Wallet) mixedSplit(ctx context.Context, req *PurchaseTicketsRequest, ne w.lockedOutpointMu.Lock() var atx *txauthor.AuthoredTx - err = walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { + err = walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) @@ -985,7 +953,8 @@ func (w *Wallet) mixedSplit(ctx context.Context, req *PurchaseTicketsRequest, ne inputSource := w.txStore.MakeInputSource(txmgrNs, addrmgrNs, req.SourceAccount, req.MinConf, tipHeight, ignoreInput) changeSource := &p2PKHChangeSource{ - persist: w.deferPersistReturnedChild(ctx, &changeSourceUpdates), + persist: w.persistReturnedChild(ctx, dbtx), + dbtx: dbtx, account: req.ChangeAccount, wallet: w, ctx: ctx, @@ -1064,11 +1033,7 @@ func (w *Wallet) individualSplit(ctx context.Context, req *PurchaseTicketsReques return } - splitPkScript, vers, err := addressScript(splitTxAddr) - if err != nil { - err = errors.E(errors.Bug, errors.Errorf("split address %v", splitTxAddr)) - return - } + vers, splitPkScript := splitTxAddr.PaymentScript() // Create the split transaction by using txToOutputs. This varies // based upon whether or not the user is using a stake pool or not. @@ -1110,11 +1075,7 @@ func (w *Wallet) vspSplit(ctx context.Context, req *PurchaseTicketsRequest, need return } - splitPkScript, vers, err := addressScript(splitTxAddr) - if err != nil { - err = errors.E(errors.Bug, errors.Errorf("split address %v", splitTxAddr)) - return - } + vers, splitPkScript := splitTxAddr.PaymentScript() // Create the split transaction by using txToOutputs. This varies // based upon whether or not the user is using a stake pool or not. @@ -1183,23 +1144,13 @@ func (w *Wallet) purchaseTickets(ctx context.Context, op errors.Op, return nil, errors.E(op, errors.Invalid, "expiry height must be above next block height") } - addrFunc := func(op errors.Op, account, branch uint32) (dcrutil.Address, error) { + addrFunc := func(dbtx walletdb.ReadWriteTx, account, branch uint32) (payments.Address, error) { const accountName = "" // not used, so can be faked. - return w.nextAddress(ctx, op, w.persistReturnedChild(ctx, nil), accountName, + persist := w.persistReturnedChild(ctx, dbtx) + return w.nextAddress(ctx, op, dbtx, persist, accountName, account, branch, WithGapPolicyIgnore()) } - if w.addressReuse && req.CSPPServer == "" { - xpub := w.addressBuffers[udb.DefaultAccountNum].albExternal.branchXpub - addr, err := deriveChildAddress(xpub, 0, w.chainParams) - if err != nil { - err = errors.E(op, err) - } - addrFunc = func(errors.Op, uint32, uint32) (dcrutil.Address, error) { - return addr, err - } - } - // Calculate the current ticket price. If the DCP0001 deployment is not // active, fallback to querying the ticket price over RPC. ticketPrice, err := w.NextStakeDifficulty(ctx) @@ -1213,7 +1164,17 @@ func (w *Wallet) purchaseTickets(ctx context.Context, op errors.Op, // Try to get the pool address from the request. If none exists // in the request, try to get the global pool address. Then do // the same for pool fees, but check sanity too. - poolAddress := req.VSPAddress + var poolAddress payments.StakeAddress + if req.VSPAddress != nil { + switch a := req.VSPAddress.(type) { + case payments.StakeAddress: + poolAddress = a + default: + err := errors.Errorf("VSP address type %T is not usable "+ + "in a ticket commitment", req.VSPAddress) + return nil, errors.E(errors.Invalid, err) + } + } if poolAddress == nil { poolAddress = w.poolAddress } @@ -1229,13 +1190,15 @@ func (w *Wallet) purchaseTickets(ctx context.Context, op errors.Op, // The stake submission pkScript is tagged by an OP_SSTX. switch req.VotingAddress.(type) { - case *dcrutil.AddressScriptHash: - stakeSubmissionPkScriptSize = txsizes.P2SHPkScriptSize + 1 - case *dcrutil.AddressPubKeyHash, PubKeyHashAddress, nil: - stakeSubmissionPkScriptSize = txsizes.P2PKHPkScriptSize + 1 + case payments.StakeAddress: + stakeSubmissionPkScriptSize = 1 + req.VotingAddress.ScriptLen() + case nil: + // generated address will be tagged p2pkh + stakeSubmissionPkScriptSize = 1 + txsizes.P2PKHPkScriptSize default: - return nil, errors.E(op, errors.Invalid, - "ticket address must either be P2SH or P2PKH") + e := errors.Errorf("voting address is unhandled type %T", + req.VotingAddress) + return nil, errors.E(op, errors.Invalid, e) } // Make sure that we have enough funds. Calculate different @@ -1396,33 +1359,50 @@ func (w *Wallet) purchaseTickets(ctx context.Context, op errors.Op, // request first, then check the ticket address // stored from the configuation. Finally, generate // an address. - addrVote := req.VotingAddress - if addrVote == nil && req.CSPPServer == "" { - addrVote = w.ticketAddress - } - if addrVote == nil { - addrVote, err = addrFunc(op, req.VotingAccount, 1) - if err != nil { - return nil, err - } - } - subsidyAccount := req.SourceAccount - var branch uint32 = 1 - if req.CSPPServer != "" { - subsidyAccount = req.MixedAccount - branch = req.MixedAccountBranch - } - addrSubsidy, err := addrFunc(op, subsidyAccount, branch) - if err != nil { - return nil, err - } - + var addrVote, addrSubsidy payments.StakeAddress var ticket *wire.MsgTx w.lockedOutpointMu.Lock() err = walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { + addrVote, _ = req.VotingAddress.(payments.StakeAddress) + if addrVote == nil && req.CSPPServer == "" { + addrVote, _ = w.ticketAddress.(payments.StakeAddress) + } + if addrVote == nil { + a, err := addrFunc(dbtx, req.VotingAccount, 1) + if err != nil { + return err + } + var ok bool + addrVote, ok = a.(payments.StakeAddress) + if !ok { + err := errors.Errorf("address type %T is not "+ + "usable as a voting address", a) + return errors.E(errors.Invalid, err) + } + } + + var subsidyAccount = req.SourceAccount + var branch uint32 = 1 + if req.CSPPServer != "" { + subsidyAccount = req.MixedAccount + branch = req.MixedAccountBranch + } + + a, err := addrFunc(dbtx, subsidyAccount, branch) + if err != nil { + return err + } + var ok bool + addrSubsidy, ok = a.(payments.StakeAddress) + if !ok { + err := errors.Errorf("address type %T is not "+ + "usable as a ticket subsidy address", a) + return errors.E(errors.Invalid, err) + } + // Generate the ticket msgTx and sign it if DontSignTx is false. ticket, err = makeTicket(w.chainParams, eopPool, eop, addrVote, - addrSubsidy, int64(ticketPrice), poolAddress) + addrSubsidy, poolAddress, int64(ticketPrice)) if err != nil { return err } diff --git a/wallet/discovery.go b/wallet/discovery.go index 909cde920..0ffeaac54 100644 --- a/wallet/discovery.go +++ b/wallet/discovery.go @@ -212,11 +212,7 @@ func (a *addrFinder) find(ctx context.Context, start *chainhash.Hash, p Peer) er return err } for i, addr := range addrs { - scr, _, err := addressScript(addr) - if err != nil { - log.Errorf("addressScript(%v): %v", addr, err) - continue - } + _, scr := addr.PaymentScript() data = append(data, scr) scrPaths[string(scr)] = scriptPath{ usageIndex: usageIndex, @@ -452,11 +448,7 @@ func (w *Wallet) findLastUsedAccount(ctx context.Context, p Peer, blockCache blo return 0, err } for _, a := range addrs { - script, _, err := addressScript(a) - if err != nil { - log.Warnf("Failed to create output script for address %v: %v", a, err) - continue - } + _, script := a.PaymentScript() addrScriptAccts[string(script)] = acct addrScripts = append(addrScripts, script) } @@ -465,11 +457,7 @@ func (w *Wallet) findLastUsedAccount(ctx context.Context, p Peer, blockCache blo return 0, err } for _, a := range addrs { - script, _, err := addressScript(a) - if err != nil { - log.Warnf("Failed to create output script for address %v: %v", a, err) - continue - } + _, script := a.PaymentScript() addrScriptAccts[string(script)] = acct addrScripts = append(addrScripts, script) } diff --git a/wallet/discovery_test.go b/wallet/discovery_test.go index 85c070055..44c5e6e58 100644 --- a/wallet/discovery_test.go +++ b/wallet/discovery_test.go @@ -8,6 +8,7 @@ import ( "context" "testing" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/wallet/walletdb" ) @@ -69,13 +70,17 @@ func TestDiscoveryCursorPos(t *testing.T) { if err != nil { t.Fatal(err) } + utilAddr4, err := payments.AddressToUtilAddress(addr4, w.chainParams) + if err != nil { + t.Fatal(err) + } err = walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { ns := dbtx.ReadBucket(waddrmgrNamespaceKey) err = w.manager.MarkReturnedChildIndex(dbtx, 0, 0, 9) // 0-9 have been returned if err != nil { return err } - maddr4, err := w.manager.Address(ns, addr4) + maddr4, err := w.manager.Address(ns, utilAddr4) if err != nil { return err } diff --git a/wallet/mixing.go b/wallet/mixing.go index 453a9d444..f62eba16f 100644 --- a/wallet/mixing.go +++ b/wallet/mixing.go @@ -13,6 +13,7 @@ import ( "decred.org/cspp" "decred.org/cspp/coinjoin" "decred.org/dcrwallet/errors" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/wallet/txauthor" "decred.org/dcrwallet/wallet/txrules" "decred.org/dcrwallet/wallet/txsizes" @@ -175,16 +176,18 @@ func (w *Wallet) MixOutput(ctx context.Context, dialTLS DialFunc, csppserver str size := txsizes.EstimateSerializeSizeFromScriptSizes(inScriptSizes, outScriptSizes, P2PKHv0Len) changeValue := remValue - txrules.FeeForSerializeSize(feeRate, size) var change *wire.TxOut - var updates []func(walletdb.ReadWriteTx) error if !txrules.IsDustAmount(changeValue, P2PKHv0Len, feeRate) { - persist := w.deferPersistReturnedChild(ctx, &updates) - const accountName = "" // not used, so can be faked. - addr, err := w.nextAddress(ctx, op, persist, - accountName, changeAccount, udb.InternalBranch, WithGapPolicyIgnore()) - if err != nil { - return errors.E(op, err) - } - changeScript, version, err := addressScript(addr) + var addr payments.Address + err = walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { + persist := w.persistReturnedChild(ctx, dbtx) + const accountName = "" // not used, so can be faked. + var err error + addr, err = w.nextAddress(ctx, op, dbtx, persist, + accountName, changeAccount, udb.InternalBranch, WithGapPolicyIgnore()) + return err + }) + + version, changeScript := addr.PaymentScript() if err != nil { return errors.E(op, err) } @@ -208,20 +211,6 @@ func (w *Wallet) MixOutput(ctx context.Context, dialTLS DialFunc, csppserver str cjHash := cj.tx.TxHash() log.Infof("Completed CoinShuffle++ mix of output %v in transaction %v", output, &cjHash) - w.lockedOutpointMu.Lock() - err = walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { - for _, f := range updates { - if err := f(dbtx); err != nil { - return err - } - } - return nil - }) - w.lockedOutpointMu.Unlock() - if err != nil { - return errors.E(op, err) - } - return nil } diff --git a/wallet/network.go b/wallet/network.go index 34ca86c17..7ae7a6291 100644 --- a/wallet/network.go +++ b/wallet/network.go @@ -8,6 +8,7 @@ import ( "context" "decred.org/dcrwallet/errors" + "decred.org/dcrwallet/payments" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/dcrutil/v3" "github.com/decred/dcrd/gcs/v2" @@ -44,7 +45,7 @@ type Peer interface { // functionality for rescanning and filtering. type NetworkBackend interface { Peer - LoadTxFilter(ctx context.Context, reload bool, addrs []dcrutil.Address, outpoints []wire.OutPoint) error + LoadTxFilter(ctx context.Context, reload bool, addrs []payments.Address, outpoints []wire.OutPoint) error Rescan(ctx context.Context, blocks []chainhash.Hash, save func(block *chainhash.Hash, txs []*wire.MsgTx) error) error // This is impossible to determine over the wire protocol, and will always diff --git a/wallet/network_test.go b/wallet/network_test.go index f4a9eba18..bdc571a7e 100644 --- a/wallet/network_test.go +++ b/wallet/network_test.go @@ -7,6 +7,7 @@ package wallet import ( "context" + "decred.org/dcrwallet/payments" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/dcrutil/v3" "github.com/decred/dcrd/wire" @@ -27,7 +28,7 @@ func (mockNetwork) Headers(ctx context.Context, blockLocators []*chainhash.Hash, return nil, nil } func (mockNetwork) PublishTransactions(ctx context.Context, txs ...*wire.MsgTx) error { return nil } -func (mockNetwork) LoadTxFilter(ctx context.Context, reload bool, addrs []dcrutil.Address, outpoints []wire.OutPoint) error { +func (mockNetwork) LoadTxFilter(ctx context.Context, reload bool, addrs []payments.Address, outpoints []wire.OutPoint) error { return nil } func (mockNetwork) Rescan(ctx context.Context, blocks []chainhash.Hash, save func(*chainhash.Hash, []*wire.MsgTx) error) error { diff --git a/wallet/rescan.go b/wallet/rescan.go index c0b25b417..e0015cf21 100644 --- a/wallet/rescan.go +++ b/wallet/rescan.go @@ -10,12 +10,11 @@ import ( "time" "decred.org/dcrwallet/errors" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/wallet/udb" "decred.org/dcrwallet/wallet/walletdb" "github.com/decred/dcrd/chaincfg/chainhash" - "github.com/decred/dcrd/dcrutil/v3" "github.com/decred/dcrd/wire" - "golang.org/x/crypto/ripemd160" ) const maxBlocksPerRescan = 2000 @@ -26,8 +25,8 @@ const maxBlocksPerRescan = 2000 // not safe for concurrent access. type RescanFilter struct { // Implemented fast paths for address lookup. - pubKeyHashes map[[ripemd160.Size]byte]struct{} - scriptHashes map[[ripemd160.Size]byte]struct{} + pubKeyHashes map[string]struct{} + scriptHashes map[string]struct{} compressedPubKeys map[[33]byte]struct{} uncompressedPubKeys map[[65]byte]struct{} @@ -42,14 +41,12 @@ type RescanFilter struct { // NewRescanFilter creates and initializes a RescanFilter containing each passed // address and outpoint. -func NewRescanFilter(addresses []dcrutil.Address, unspentOutPoints []*wire.OutPoint) *RescanFilter { +func NewRescanFilter(addresses []payments.Address, unspentOutPoints []*wire.OutPoint) *RescanFilter { filter := &RescanFilter{ - pubKeyHashes: map[[ripemd160.Size]byte]struct{}{}, - scriptHashes: map[[ripemd160.Size]byte]struct{}{}, - compressedPubKeys: map[[33]byte]struct{}{}, - uncompressedPubKeys: map[[65]byte]struct{}{}, - otherAddresses: map[string]struct{}{}, - unspent: make(map[wire.OutPoint]struct{}, len(unspentOutPoints)), + pubKeyHashes: map[string]struct{}{}, + scriptHashes: map[string]struct{}{}, + otherAddresses: map[string]struct{}{}, + unspent: make(map[wire.OutPoint]struct{}, len(unspentOutPoints)), } for _, s := range addresses { @@ -63,81 +60,39 @@ func NewRescanFilter(addresses []dcrutil.Address, unspentOutPoints []*wire.OutPo } // AddAddress adds an address to the filter if it does not already exist. -func (f *RescanFilter) AddAddress(a dcrutil.Address) { +func (f *RescanFilter) AddAddress(a payments.Address) { switch a := a.(type) { - case *dcrutil.AddressPubKeyHash: - f.pubKeyHashes[*a.Hash160()] = struct{}{} - case *dcrutil.AddressScriptHash: - f.scriptHashes[*a.Hash160()] = struct{}{} - case *dcrutil.AddressSecpPubKey: - serializedPubKey := a.ScriptAddress() - switch len(serializedPubKey) { - case 33: // compressed - var compressedPubKey [33]byte - copy(compressedPubKey[:], serializedPubKey) - f.compressedPubKeys[compressedPubKey] = struct{}{} - case 65: // uncompressed - var uncompressedPubKey [65]byte - copy(uncompressedPubKey[:], serializedPubKey) - f.uncompressedPubKeys[uncompressedPubKey] = struct{}{} - } + case payments.PubKeyHashAddress: + f.pubKeyHashes[string(a.PubKeyHash())] = struct{}{} + case payments.ScriptHashAddress: + f.scriptHashes[string(a.ScriptHash())] = struct{}{} default: - f.otherAddresses[a.Address()] = struct{}{} + f.otherAddresses[a.String()] = struct{}{} } } // ExistsAddress returns whether an address is contained in the filter. -func (f *RescanFilter) ExistsAddress(a dcrutil.Address) (ok bool) { +func (f *RescanFilter) ExistsAddress(a payments.Address) (ok bool) { switch a := a.(type) { - case *dcrutil.AddressPubKeyHash: - _, ok = f.pubKeyHashes[*a.Hash160()] - case *dcrutil.AddressScriptHash: - _, ok = f.scriptHashes[*a.Hash160()] - case *dcrutil.AddressSecpPubKey: - serializedPubKey := a.ScriptAddress() - switch len(serializedPubKey) { - case 33: // compressed - var compressedPubKey [33]byte - copy(compressedPubKey[:], serializedPubKey) - _, ok = f.compressedPubKeys[compressedPubKey] - if !ok { - _, ok = f.pubKeyHashes[*a.AddressPubKeyHash().Hash160()] - } - case 65: // uncompressed - var uncompressedPubKey [65]byte - copy(uncompressedPubKey[:], serializedPubKey) - _, ok = f.uncompressedPubKeys[uncompressedPubKey] - if !ok { - _, ok = f.pubKeyHashes[*a.AddressPubKeyHash().Hash160()] - } - } + case payments.PubKeyHashAddress: + _, ok = f.pubKeyHashes[string(a.PubKeyHash())] + case payments.ScriptHashAddress: + _, ok = f.scriptHashes[string(a.ScriptHash())] default: - _, ok = f.otherAddresses[a.Address()] + _, ok = f.otherAddresses[a.String()] } return } // RemoveAddress removes an address from the filter if it exists. -func (f *RescanFilter) RemoveAddress(a dcrutil.Address) { +func (f *RescanFilter) RemoveAddress(a payments.Address) { switch a := a.(type) { - case *dcrutil.AddressPubKeyHash: - delete(f.pubKeyHashes, *a.Hash160()) - case *dcrutil.AddressScriptHash: - delete(f.scriptHashes, *a.Hash160()) - case *dcrutil.AddressSecpPubKey: - serializedPubKey := a.ScriptAddress() - switch len(serializedPubKey) { - case 33: // compressed - var compressedPubKey [33]byte - copy(compressedPubKey[:], serializedPubKey) - delete(f.compressedPubKeys, compressedPubKey) - case 65: // uncompressed - var uncompressedPubKey [65]byte - copy(uncompressedPubKey[:], serializedPubKey) - delete(f.uncompressedPubKeys, uncompressedPubKey) - } + case payments.PubKeyHashAddress: + delete(f.pubKeyHashes, string(a.PubKeyHash())) + case payments.ScriptHashAddress: + delete(f.scriptHashes, string(a.ScriptHash())) default: - delete(f.otherAddresses, a.Address()) + delete(f.otherAddresses, a.String()) } } diff --git a/wallet/udb/txmined.go b/wallet/udb/txmined.go index 68f5b0a3b..0374d6779 100644 --- a/wallet/udb/txmined.go +++ b/wallet/udb/txmined.go @@ -2631,15 +2631,16 @@ func (s *Store) GetMultisigOutput(ns walletdb.ReadBucket, op *wire.OutPoint) (*M // UnspentMultisigCreditsForAddress returns all unspent multisignature P2SH // credits in the wallet for some specified address. -func (s *Store) UnspentMultisigCreditsForAddress(dbtx walletdb.ReadTx, addr dcrutil.Address) ([]*MultisigCredit, error) { +func (s *Store) UnspentMultisigCreditsForAddress(dbtx walletdb.ReadTx, p2shScriptHash []byte) ([]*MultisigCredit, error) { ns := dbtx.ReadBucket(wtxmgrBucketKey) addrmgrNs := dbtx.ReadBucket(waddrmgrBucketKey) - p2shAddr, ok := addr.(*dcrutil.AddressScriptHash) - if !ok { - return nil, errors.E(errors.Invalid, "address must be P2SH") + if len(p2shScriptHash) != 20 { + err := errors.Errorf("P2SH script hash is invalid length %d", len(p2shScriptHash)) + return nil, errors.E(errors.Invalid, err) } - addrScrHash := p2shAddr.Hash160() + var addrScrHash [20]byte + copy(addrScrHash[:], p2shScriptHash) var mscs []*MultisigCredit c := ns.NestedReadBucket(bucketMultisigUsp).ReadCursor() @@ -2653,7 +2654,7 @@ func (s *Store) UnspentMultisigCreditsForAddress(dbtx walletdb.ReadTx, addr dcru // Skip everything that's unrelated to the address // we're concerned about. scriptHash := fetchMultisigOutScrHash(val) - if scriptHash != *addrScrHash { + if scriptHash != addrScrHash { continue } diff --git a/wallet/unstable.go b/wallet/unstable.go index d7751245e..5b8d38538 100644 --- a/wallet/unstable.go +++ b/wallet/unstable.go @@ -8,10 +8,10 @@ import ( "context" "decred.org/dcrwallet/errors" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/wallet/udb" "decred.org/dcrwallet/wallet/walletdb" "github.com/decred/dcrd/chaincfg/chainhash" - "github.com/decred/dcrd/dcrutil/v3" ) type unstableAPI struct { @@ -59,13 +59,24 @@ func (u unstableAPI) RangeTransactions(ctx context.Context, begin, end int32, f // UnspentMultisigCreditsForAddress calls // udb.Store.UnspentMultisigCreditsForAddress under a single database view // transaction. -func (u unstableAPI) UnspentMultisigCreditsForAddress(ctx context.Context, p2shAddr *dcrutil.AddressScriptHash) ([]*udb.MultisigCredit, error) { +func (u unstableAPI) UnspentMultisigCreditsForAddress(ctx context.Context, + p2shAddr payments.Address) ([]*udb.MultisigCredit, error) { const op errors.Op = "wallet.UnspentMultisigCreditsForAddress" + + var scriptHash []byte + switch a := p2shAddr.(type) { + case payments.ScriptHashAddress: + scriptHash = a.ScriptHash() + default: + err := errors.Errorf("address %v (type %[1]T) does not implement ScriptHashAddress", p2shAddr) + return nil, err + } + var multisigCredits []*udb.MultisigCredit err := walletdb.View(ctx, u.w.db, func(tx walletdb.ReadTx) error { var err error multisigCredits, err = u.w.txStore.UnspentMultisigCreditsForAddress( - tx, p2shAddr) + tx, scriptHash) return err }) if err != nil { diff --git a/wallet/wallet.go b/wallet/wallet.go index 12bf82603..52d5e33c5 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -19,7 +19,7 @@ import ( "decred.org/dcrwallet/deployments" "decred.org/dcrwallet/errors" - "decred.org/dcrwallet/internal/compat" + "decred.org/dcrwallet/payments" "decred.org/dcrwallet/rpc/client/dcrd" "decred.org/dcrwallet/rpc/jsonrpc/types" "decred.org/dcrwallet/validate" @@ -96,7 +96,7 @@ type Wallet struct { stakeSettingsLock sync.Mutex defaultVoteBits stake.VoteBits votingEnabled bool - poolAddress dcrutil.Address + poolAddress payments.StakeAddress poolFees float64 manualTickets bool stakePoolEnabled bool @@ -121,8 +121,7 @@ type Wallet struct { recentlyPublishedMu sync.Mutex // Internal address handling. - addressReuse bool - ticketAddress dcrutil.Address + ticketAddress payments.StakeAddress addressBuffers map[uint32]*bip0044AccountData addressBuffersMu sync.Mutex @@ -143,9 +142,8 @@ type Config struct { PubPassphrase []byte VotingEnabled bool - AddressReuse bool - VotingAddress dcrutil.Address - PoolAddress dcrutil.Address + VotingAddress payments.Address + PoolAddress payments.Address PoolFees float64 GapLimit uint32 @@ -687,7 +685,7 @@ func (w *Wallet) watchHDAddrs(ctx context.Context, firstWatch bool, n NetworkBac ctx, cancel := context.WithCancel(ctx) defer cancel() - watchAddrs := make(chan []dcrutil.Address, runtime.NumCPU()) + watchAddrs := make(chan []payments.Address, runtime.NumCPU()) watchError := make(chan error) go func() { for addrs := range watchAddrs { @@ -705,7 +703,7 @@ func (w *Wallet) watchHDAddrs(ctx context.Context, firstWatch bool, n NetworkBac loadBranchAddrs := func(branchKey *hdkeychain.ExtendedKey, start, end uint32) { const step = 256 for ; start <= end; start += step { - addrs := make([]dcrutil.Address, 0, step) + addrs := make([]payments.Address, 0, step) stop := minUint32(end+1, start+step) for child := start; child < stop; child++ { addr, err := deriveChildAddress(branchKey, child, w.chainParams) @@ -809,15 +807,24 @@ func (w *Wallet) LoadActiveDataFilters(ctx context.Context, n NetworkBackend, re } log.Infof("Registered for transaction notifications for %v HD address(es)", hdAddrCount) - // Watch individually-imported addresses (which must each be read out of - // the DB). - abuf := make([]dcrutil.Address, 0, 256) + // Watch individually-imported and hardened account addresses (which + // must each be read out of the DB). + abuf := make([]payments.Address, 0, 256) var importedAddrCount int watchAddress := func(a udb.ManagedAddress) error { - addr := a.Address() + const accountName = "" // not used; can be faked + importedAddrCount++ + var kind AccountKind = AccountKindImported + if a.Account() == udb.ImportedAddrAccount { + } else { + } + addr, err := wrapManagedAddress(a, accountName, kind) + if err != nil { + return err + } abuf = append(abuf, addr) if len(abuf) == cap(abuf) { - importedAddrCount += len(abuf) + log.Infof("acct %d watching addrs %v", a.Account(), abuf) err := n.LoadTxFilter(ctx, false, abuf, nil) abuf = abuf[:0] return err @@ -825,8 +832,19 @@ func (w *Wallet) LoadActiveDataFilters(ctx context.Context, n NetworkBackend, re return nil } err = walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { - addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) - return w.manager.ForEachAccountAddress(addrmgrNs, udb.ImportedAddrAccount, watchAddress) + ns := dbtx.ReadBucket(waddrmgrNamespaceKey) + + lastImported, err := w.manager.LastImportedAccount(dbtx) + if err != nil { + return err + } + for acct := uint32(udb.ImportedAddrAccount); acct <= lastImported; acct++ { + err = w.manager.ForEachAccountAddress(ns, acct, watchAddress) + if err != nil { + return err + } + } + return nil }) if err != nil { return err @@ -878,10 +896,10 @@ func (w *Wallet) LoadActiveDataFilters(ctx context.Context, n NetworkBackend, re // CommittedTickets takes a list of tickets and returns a filtered list of // tickets that are controlled by this wallet. -func (w *Wallet) CommittedTickets(ctx context.Context, tickets []*chainhash.Hash) ([]*chainhash.Hash, []dcrutil.Address, error) { +func (w *Wallet) CommittedTickets(ctx context.Context, tickets []*chainhash.Hash) ([]*chainhash.Hash, []payments.Address, error) { const op errors.Op = "wallet.CommittedTickets" hashes := make([]*chainhash.Hash, 0, len(tickets)) - addresses := make([]dcrutil.Address, 0, len(tickets)) + addresses := make([]payments.Address, 0, len(tickets)) // Verify we own this ticket err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) @@ -899,18 +917,19 @@ func (w *Wallet) CommittedTickets(ctx context.Context, tickets []*chainhash.Hash // Commitment outputs are at alternating output // indexes, starting at 1. - var bestAddr dcrutil.Address + var bestAddr payments.PubKeyHashAddress var bestAmount dcrutil.Amount for i := 1; i < len(tx.TxOut); i += 2 { scr := tx.TxOut[i].PkScript - addr, err := stake.AddrFromSStxPkScrCommitment(scr, + addr, err := payments.ParseTicketCommitmentAddress(scr, w.chainParams) if err != nil { log.Debugf("%v", err) break } - if _, ok := addr.(*dcrutil.AddressPubKeyHash); !ok { + pkha, ok := addr.(payments.PubKeyHashAddress) + if !ok { log.Tracef("Skipping commitment at "+ "index %v: address is not "+ "P2PKH", i) @@ -922,7 +941,7 @@ func (w *Wallet) CommittedTickets(ctx context.Context, tickets []*chainhash.Hash break } if amt > bestAmount { - bestAddr = addr + bestAddr = pkha bestAmount = amt } } @@ -933,9 +952,8 @@ func (w *Wallet) CommittedTickets(ctx context.Context, tickets []*chainhash.Hash } if !w.manager.ExistsHash160(addrmgrNs, - bestAddr.Hash160()[:]) { - log.Debugf("not our address: %x", - bestAddr.Hash160()) + bestAddr.PubKeyHash()) { + log.Debugf("not our address: %s", bestAddr) continue } ticketHash := tx.TxHash() @@ -1346,7 +1364,8 @@ func (w *Wallet) FetchHeaders(ctx context.Context, p Peer) (count int, rescanFro // Consolidate consolidates as many UTXOs as are passed in the inputs argument. // If that many UTXOs can not be found, it will use the maximum it finds. This // will only compress UTXOs in the default account -func (w *Wallet) Consolidate(ctx context.Context, inputs int, account uint32, address dcrutil.Address) (*chainhash.Hash, error) { +func (w *Wallet) Consolidate(ctx context.Context, inputs int, account uint32, + address payments.Address) (*chainhash.Hash, error) { heldUnlock, err := w.holdUnlock() if err != nil { return nil, err @@ -1357,7 +1376,7 @@ func (w *Wallet) Consolidate(ctx context.Context, inputs int, account uint32, ad // CreateMultisigTx creates and signs a multisig transaction. func (w *Wallet) CreateMultisigTx(ctx context.Context, account uint32, amount dcrutil.Amount, - pubkeys []*dcrutil.AddressSecpPubKey, nrequired int8, minconf int32) (*CreatedTx, dcrutil.Address, []byte, error) { + pubkeys []*dcrutil.AddressSecpPubKey, nrequired int8, minconf int32) (*CreatedTx, payments.Address, []byte, error) { heldUnlock, err := w.holdUnlock() if err != nil { return nil, nil, nil, err @@ -1370,7 +1389,7 @@ func (w *Wallet) CreateMultisigTx(ctx context.Context, account uint32, amount dc type PurchaseTicketsRequest struct { Count int SourceAccount uint32 - VotingAddress dcrutil.Address + VotingAddress payments.Address MinConf int32 Expiry int32 VotingAccount uint32 // Used when VotingAddress == nil, or CSPPServer != "" @@ -1385,7 +1404,7 @@ type PurchaseTicketsRequest struct { ChangeAccount uint32 // VSP ticket buying; not currently usable with CoinShuffle++. - VSPAddress dcrutil.Address + VSPAddress payments.Address VSPFees float64 } @@ -1629,12 +1648,13 @@ func (w *Wallet) AccountBalances(ctx context.Context, confirms int32) ([]Balance return balances, nil } -// CurrentAddress gets the most recently requested payment address from a wallet. -// If the address has already been used (there is at least one transaction -// spending to it in the blockchain or dcrd mempool), the next chained address -// is returned. -func (w *Wallet) CurrentAddress(account uint32) (dcrutil.Address, error) { +// CurrentAddress gets the most recently external payment address from an +// account. If the address has already been used (there is at least one +// transaction spending to it in the blockchain or mempool), the next account +// address is returned. +func (w *Wallet) CurrentAddress(ctx context.Context, account uint32) (payments.Address, error) { const op errors.Op = "wallet.CurrentAddress" + defer w.addressBuffersMu.Unlock() w.addressBuffersMu.Lock() @@ -1649,17 +1669,18 @@ func (w *Wallet) CurrentAddress(account uint32) (dcrutil.Address, error) { if err != nil { return nil, errors.E(op, err) } - addr, err := compat.HD2Address(child, w.chainParams) + pkh := child.SerializedPubKey() + pkha, err := payments.P2PKHAddress(pkh, w.chainParams) if err != nil { return nil, errors.E(op, err) } - return addr, nil + return pkha, nil } // SignHashes returns signatures of signed transaction hashes using an // address' associated private key. -func (w *Wallet) SignHashes(ctx context.Context, hashes [][]byte, addr dcrutil.Address) ([][]byte, - []byte, error) { +func (w *Wallet) SignHashes(ctx context.Context, hashes [][]byte, + addr payments.Address) ([][]byte, []byte, error) { var privKey *secp256k1.PrivateKey var done func() @@ -1670,8 +1691,11 @@ func (w *Wallet) SignHashes(ctx context.Context, hashes [][]byte, addr dcrutil.A }() err := walletdb.View(ctx, w.db, func(tx walletdb.ReadTx) error { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) - var err error - privKey, done, err = w.manager.PrivateKey(addrmgrNs, addr) + utilAddr, err := payments.AddressToUtilAddress(addr, w.chainParams) + if err != nil { + return err + } + privKey, done, err = w.manager.PrivateKey(addrmgrNs, utilAddr) return err }) if err != nil { @@ -1689,7 +1713,7 @@ func (w *Wallet) SignHashes(ctx context.Context, hashes [][]byte, addr dcrutil.A // SignMessage returns the signature of a signed message using an address' // associated private key. -func (w *Wallet) SignMessage(ctx context.Context, msg string, addr dcrutil.Address) (sig []byte, err error) { +func (w *Wallet) SignMessage(ctx context.Context, msg string, addr payments.Address) (sig []byte, err error) { const op errors.Op = "wallet.SignMessage" var buf bytes.Buffer wire.WriteVarString(&buf, 0, "Decred Signed Message:\n") @@ -1704,8 +1728,11 @@ func (w *Wallet) SignMessage(ctx context.Context, msg string, addr dcrutil.Addre }() err = walletdb.View(ctx, w.db, func(tx walletdb.ReadTx) error { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) - var err error - privKey, done, err = w.manager.PrivateKey(addrmgrNs, addr) + utilAddr, err := payments.AddressToUtilAddress(addr, w.chainParams) + if err != nil { + return err + } + privKey, done, err = w.manager.PrivateKey(addrmgrNs, utilAddr) return err }) if err != nil { @@ -1717,7 +1744,7 @@ func (w *Wallet) SignMessage(ctx context.Context, msg string, addr dcrutil.Addre // VerifyMessage verifies that sig is a valid signature of msg and was created // using the secp256k1 private key for addr. -func VerifyMessage(msg string, addr dcrutil.Address, sig []byte, params dcrutil.AddressParams) (bool, error) { +func VerifyMessage(msg string, addr payments.Address, sig []byte, params dcrutil.AddressParams) (bool, error) { const op errors.Op = "wallet.VerifyMessage" // Validate the signature - this just shows that it was valid for any pubkey // at all. Whether the pubkey matches is checked below. @@ -1743,15 +1770,41 @@ func VerifyMessage(msg string, addr dcrutil.Address, sig []byte, params dcrutil. } // Return whether addresses match. - return recoveredAddr.Address() == addr.Address(), nil + return recoveredAddr.Address() == addr.String(), nil } -// HaveAddress returns whether the wallet is the owner of the address a. -func (w *Wallet) HaveAddress(ctx context.Context, a dcrutil.Address) (bool, error) { +// HaveAddress returns whether the wallet manages the address addr. +func (w *Wallet) HaveAddress(ctx context.Context, addr payments.Address) (bool, error) { const op errors.Op = "wallet.HaveAddress" - err := walletdb.View(ctx, w.db, func(tx walletdb.ReadTx) error { - addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) - _, err := w.manager.Address(addrmgrNs, a) + utilAddr, err := payments.AddressToUtilAddress(addr, w.chainParams) + if err != nil { + return false, errors.E(op, err) + } + err = walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + _, err := w.manager.Address(addrmgrNs, utilAddr) + return err + }) + if err != nil { + if errors.Is(err, errors.NotExist) { + return false, nil + } + return false, errors.E(op, err) + } + return true, nil +} + +// HavePubkey returns whether the wallet manages a public key. +func (w *Wallet) HavePubkey(ctx context.Context, pubkey []byte) (bool, error) { + const op errors.Op = "wallet.HavePubkey" + pubkeyHash := dcrutil.Hash160(pubkey) + utilAddr, err := dcrutil.NewAddressPubKeyHash(pubkeyHash, w.chainParams, dcrec.STEcdsaSecp256k1) + if err != nil { + return false, errors.E(op, err) + } + err = walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + _, err := w.manager.Address(addrmgrNs, utilAddr) return err }) if err != nil { @@ -2258,7 +2311,7 @@ func (w *Wallet) ListTransactions(ctx context.Context, from, count int) ([]types // ListAddressTransactions returns a slice of objects with details about // recorded transactions to or from any address belonging to a set. This is // intended to be used for listaddresstransactions RPC replies. -func (w *Wallet) ListAddressTransactions(ctx context.Context, pkHashes map[string]struct{}) ([]types.ListTransactionsResult, error) { +func (w *Wallet) ListAddressTransactions(ctx context.Context, addrSet map[string]struct{}) ([]types.ListTransactionsResult, error) { const op errors.Op = "wallet.ListAddressTransactions" txList := []types.ListTransactionsResult{} err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { @@ -2283,7 +2336,7 @@ func (w *Wallet) ListAddressTransactions(ctx context.Context, pkHashes map[strin if !ok { continue } - _, ok = pkHashes[string(apkh.ScriptAddress())] + _, ok = addrSet[apkh.Address()] if !ok { continue } @@ -3311,7 +3364,7 @@ func (w *Wallet) ListUnspent(ctx context.Context, minconf, maxconf int32, addres // DumpWIFPrivateKey returns the WIF encoded private key for a // single wallet address. -func (w *Wallet) DumpWIFPrivateKey(ctx context.Context, addr dcrutil.Address) (string, error) { +func (w *Wallet) DumpWIFPrivateKey(ctx context.Context, addr payments.Address) (string, error) { const op errors.Op = "wallet.DumpWIFPrivateKey" var privKey *secp256k1.PrivateKey var done func() @@ -3320,10 +3373,14 @@ func (w *Wallet) DumpWIFPrivateKey(ctx context.Context, addr dcrutil.Address) (s done() } }() - err := walletdb.View(ctx, w.db, func(tx walletdb.ReadTx) error { + utilAddr, err := payments.AddressToUtilAddress(addr, w.chainParams) + if err != nil { + return "", errors.E(op, err) + } + err = walletdb.View(ctx, w.db, func(tx walletdb.ReadTx) error { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) var err error - privKey, done, err = w.manager.PrivateKey(addrmgrNs, addr) + privKey, done, err = w.manager.PrivateKey(addrmgrNs, utilAddr) return err }) if err != nil { @@ -3342,13 +3399,16 @@ func (w *Wallet) DumpWIFPrivateKey(ctx context.Context, addr dcrutil.Address) (s func (w *Wallet) ImportPrivateKey(ctx context.Context, wif *dcrutil.WIF) (string, error) { const op errors.Op = "wallet.ImportPrivateKey" // Attempt to import private key into wallet. - var addr dcrutil.Address + var addr payments.Address var props *udb.AccountProperties err := walletdb.Update(ctx, w.db, func(tx walletdb.ReadWriteTx) error { addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) maddr, err := w.manager.ImportPrivateKey(addrmgrNs, wif) + if err != nil { + return err + } + addr, err = wrapManagedAddress(maddr, "imported", AccountKindImported) if err == nil { - addr = maddr.Address() props, err = w.manager.AccountProperties( addrmgrNs, udb.ImportedAddrAccount) } @@ -3359,13 +3419,13 @@ func (w *Wallet) ImportPrivateKey(ctx context.Context, wif *dcrutil.WIF) (string } if n, err := w.NetworkBackend(); err == nil { - err := n.LoadTxFilter(ctx, false, []dcrutil.Address{addr}, nil) + err := n.LoadTxFilter(ctx, false, []payments.Address{addr}, nil) if err != nil { return "", errors.E(op, err) } } - addrStr := addr.Address() + addrStr := addr.String() log.Infof("Imported payment address %s", addrStr) w.NtfnServer.notifyAccountProperties(props) @@ -3386,9 +3446,12 @@ func (w *Wallet) ImportScript(ctx context.Context, rs []byte) error { return err } - addr := mscriptaddr.Address() + addr, err := wrapManagedAddress(mscriptaddr, "imported", AccountKindImported) + if err != nil { + return err + } if n, err := w.NetworkBackend(); err == nil { - addrs := []dcrutil.Address{addr} + addrs := []payments.Address{addr} err := n.LoadTxFilter(ctx, false, addrs, nil) if err != nil { return err @@ -4051,7 +4114,8 @@ func (w *Wallet) TotalReceivedForAccounts(ctx context.Context, minConf int32) ([ // TotalReceivedForAddr iterates through a wallet's transaction history, // returning the total amount of decred received for a single wallet // address. -func (w *Wallet) TotalReceivedForAddr(ctx context.Context, addr dcrutil.Address, minConf int32) (dcrutil.Amount, error) { +func (w *Wallet) TotalReceivedForAddr(ctx context.Context, addr payments.Address, + minConf int32) (dcrutil.Amount, error) { const op errors.Op = "wallet.TotalReceivedForAddr" var amount dcrutil.Amount err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { @@ -4060,7 +4124,7 @@ func (w *Wallet) TotalReceivedForAddr(ctx context.Context, addr dcrutil.Address, _, tipHeight := w.txStore.MainChainTip(dbtx) var ( - addrStr = addr.Address() + addrStr = addr.String() stopHeight int32 ) @@ -4298,7 +4362,7 @@ func (w *Wallet) SignTransaction(ctx context.Context, tx *wire.MsgTx, hashType t // CreateSignature returns the raw signature created by the private key of addr // for tx's idx'th input script and the serialized compressed pubkey for the // address. -func (w *Wallet) CreateSignature(ctx context.Context, tx *wire.MsgTx, idx uint32, addr dcrutil.Address, +func (w *Wallet) CreateSignature(ctx context.Context, tx *wire.MsgTx, idx uint32, addr payments.Address, hashType txscript.SigHashType, prevPkScript []byte) (sig, pubkey []byte, err error) { const op errors.Op = "wallet.CreateSignature" var privKey *secp256k1.PrivateKey @@ -4313,7 +4377,10 @@ func (w *Wallet) CreateSignature(ctx context.Context, tx *wire.MsgTx, idx uint32 err = walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { ns := dbtx.ReadBucket(waddrmgrNamespaceKey) - var err error + addr, err := payments.AddressToUtilAddress(addr, w.chainParams) + if err != nil { + return err + } privKey, done, err = w.manager.PrivateKey(ns, addr) if err != nil { return err @@ -4422,7 +4489,7 @@ func (w *Wallet) appendRelevantOutpoints(relevant []wire.OutPoint, dbtx walletdb switch class { case txscript.StakeSubmissionTy, txscript.StakeSubChangeTy, txscript.StakeGenTy, txscript.StakeRevocationTy: - tree = wire.TxTreeStake + op.Tree = wire.TxTreeStake } for _, a := range addrs { @@ -4765,7 +4832,7 @@ func decodeStakePoolColdExtKey(encStr string, params *chaincfg.Params) (map[stri addrMap := make(map[string]struct{}) for i := range addrs { - addrMap[addrs[i].Address()] = struct{}{} + addrMap[addrs[i].String()] = struct{}{} } return addrMap, nil @@ -4807,14 +4874,31 @@ func Open(ctx context.Context, cfg *Config) (*Wallet, error) { return nil, errors.E(op, err) } + var votingAddress, poolAddress payments.StakeAddress + switch addr := cfg.VotingAddress.(type) { + case nil: + case payments.StakeAddress: + votingAddress = addr + default: + err := errors.Errorf("%v is not usable as a voting address", addr) + return nil, errors.E(op, err) + } + switch addr := cfg.PoolAddress.(type) { + case nil: + case payments.StakeAddress: + poolAddress = addr + default: + err := errors.Errorf("%v is not usable as a VSP fee commitment address", addr) + return nil, errors.E(op, err) + } + w := &Wallet{ db: db, // StakeOptions votingEnabled: cfg.VotingEnabled, - addressReuse: cfg.AddressReuse, - ticketAddress: cfg.VotingAddress, - poolAddress: cfg.PoolAddress, + ticketAddress: votingAddress, + poolAddress: poolAddress, poolFees: cfg.PoolFees, // LoaderOptions