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