wallet: require key scope of account for transaction creation methods

Now that we're able to fund transactions from multiple accounts within
different key scopes, we extend our transaction creation methods to
accept a key scope parameter as well, to determine the correct account
to select inputs from.
This commit is contained in:
Wilmer Paulino 2021-02-17 16:24:14 -08:00
parent 7fa80abc44
commit 2301069644
No known key found for this signature in database
GPG key ID: 6DF57B9F9514972F
7 changed files with 106 additions and 57 deletions

View file

@ -1359,13 +1359,14 @@ func makeOutputs(pairs map[string]btcutil.Amount, chainParams *chaincfg.Params)
// It returns the transaction hash in string format upon success
// All errors are returned in btcjson.RPCError format
func sendPairs(w *wallet.Wallet, amounts map[string]btcutil.Amount,
account uint32, minconf int32, feeSatPerKb btcutil.Amount) (string, error) {
keyScope waddrmgr.KeyScope, account uint32, minconf int32,
feeSatPerKb btcutil.Amount) (string, error) {
outputs, err := makeOutputs(amounts, w.ChainParams())
if err != nil {
return "", err
}
tx, err := w.SendOutputs(outputs, account, minconf, feeSatPerKb, "")
tx, err := w.SendOutputs(outputs, &keyScope, account, minconf, feeSatPerKb, "")
if err != nil {
if err == txrules.ErrAmountNegative {
return "", ErrNeedPositiveAmount
@ -1433,7 +1434,7 @@ func sendFrom(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient)
cmd.ToAddress: amt,
}
return sendPairs(w, pairs, account, minConf,
return sendPairs(w, pairs, waddrmgr.KeyScopeBIP0044, account, minConf,
txrules.DefaultRelayFeePerKb)
}
@ -1475,7 +1476,7 @@ func sendMany(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
pairs[k] = amt
}
return sendPairs(w, pairs, account, minConf, txrules.DefaultRelayFeePerKb)
return sendPairs(w, pairs, waddrmgr.KeyScopeBIP0044, account, minConf, txrules.DefaultRelayFeePerKb)
}
// sendToAddress handles a sendtoaddress RPC request by creating a new
@ -1511,7 +1512,7 @@ func sendToAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
}
// sendtoaddress always spends from the default account, this matches bitcoind
return sendPairs(w, pairs, waddrmgr.DefaultAccountNum, 1,
return sendPairs(w, pairs, waddrmgr.KeyScopeBIP0044, waddrmgr.DefaultAccountNum, 1,
txrules.DefaultRelayFeePerKb)
}

View file

@ -99,14 +99,17 @@ func (s secretSource) GetScript(addr btcutil.Address) ([]byte, error) {
// txToOutputs creates a signed transaction which includes each output from
// outputs. Previous outputs to reedeem are chosen from the passed account's
// UTXO set and minconf policy. An additional output may be added to return
// change to the wallet. An appropriate fee is included based on the wallet's
// current relay fee. The wallet must be unlocked to create the transaction.
// change to the wallet. This output will have an address generated from the
// given key scope and account. If a key scope is not specified, the address
// will always be generated from the P2WKH key scope. An appropriate fee is
// included based on the wallet's current relay fee. The wallet must be
// unlocked to create the transaction.
//
// NOTE: The dryRun argument can be set true to create a tx that doesn't alter
// the database. A tx created with this set to true will intentionally have no
// input scripts added and SHOULD NOT be broadcasted.
func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32,
minconf int32, feeSatPerKb btcutil.Amount, dryRun bool) (
func (w *Wallet) txToOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
account uint32, minconf int32, feeSatPerKb btcutil.Amount, dryRun bool) (
tx *txauthor.AuthoredTx, err error) {
chainClient, err := w.requireChainClient()
@ -120,7 +123,9 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32,
}
defer func() { _ = dbtx.Rollback() }()
addrmgrNs, changeSource := w.addrMgrWithChangeSource(dbtx, account)
addrmgrNs, changeSource := w.addrMgrWithChangeSource(
dbtx, keyScope, account,
)
// Get current block's height and hash.
bs, err := chainClient.BlockStamp()
@ -128,14 +133,17 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32,
return nil, err
}
eligible, err := w.findEligibleOutputs(dbtx, account, minconf, bs)
eligible, err := w.findEligibleOutputs(
dbtx, keyScope, account, minconf, bs,
)
if err != nil {
return nil, err
}
inputSource := makeInputSource(eligible)
tx, err = txauthor.NewUnsignedTransaction(outputs, feeSatPerKb,
inputSource, changeSource)
tx, err = txauthor.NewUnsignedTransaction(
outputs, feeSatPerKb, inputSource, changeSource,
)
if err != nil {
return nil, err
}
@ -193,7 +201,10 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32,
return tx, nil
}
func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx, account uint32, minconf int32, bs *waddrmgr.BlockStamp) ([]wtxmgr.Credit, error) {
func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx,
keyScope *waddrmgr.KeyScope, account uint32, minconf int32,
bs *waddrmgr.BlockStamp) ([]wtxmgr.Credit, error) {
addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey)
txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey)
@ -239,8 +250,14 @@ func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx, account uint32, minco
if err != nil || len(addrs) != 1 {
continue
}
_, addrAcct, err := w.Manager.AddrAccount(addrmgrNs, addrs[0])
if err != nil || addrAcct != account {
scopedMgr, addrAcct, err := w.Manager.AddrAccount(addrmgrNs, addrs[0])
if err != nil {
continue
}
if keyScope != nil && scopedMgr.Scope() != *keyScope {
continue
}
if addrAcct != account {
continue
}
eligible = append(eligible, *output)
@ -249,9 +266,13 @@ func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx, account uint32, minco
}
// addrMgrWithChangeSource returns the address manager bucket and a change
// source function that returns change addresses from said address manager.
// source function that returns change addresses from said address manager. The
// change addresses will come from the specified key scope and account, unless
// a key scope is not specified. In that case, change addresses will always
// come from the P2WKH key scope.
func (w *Wallet) addrMgrWithChangeSource(dbtx walletdb.ReadWriteTx,
account uint32) (walletdb.ReadWriteBucket, txauthor.ChangeSource) {
changeKeyScope *waddrmgr.KeyScope, account uint32) (walletdb.ReadWriteBucket,
txauthor.ChangeSource) {
addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey)
changeSource := func() ([]byte, error) {
@ -261,14 +282,16 @@ func (w *Wallet) addrMgrWithChangeSource(dbtx walletdb.ReadWriteTx,
// are created from account 0.
var changeAddr btcutil.Address
var err error
changeKeyScope := waddrmgr.KeyScopeBIP0084
if changeKeyScope == nil {
changeKeyScope = &waddrmgr.KeyScopeBIP0084
}
if account == waddrmgr.ImportedAddrAccount {
changeAddr, err = w.newChangeAddress(
addrmgrNs, 0, changeKeyScope,
addrmgrNs, 0, *changeKeyScope,
)
} else {
changeAddr, err = w.newChangeAddress(
addrmgrNs, account, changeKeyScope,
addrmgrNs, account, *changeKeyScope,
)
}
if err != nil {

View file

@ -34,7 +34,8 @@ func TestTxToOutputsDryRun(t *testing.T) {
defer cleanup()
// Create an address we can use to send some coins to.
addr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0044)
keyScope := waddrmgr.KeyScopeBIP0049Plus
addr, err := w.CurrentAddress(0, keyScope)
if err != nil {
t.Fatalf("unable to get current address: %v", addr)
}
@ -70,7 +71,7 @@ func TestTxToOutputsDryRun(t *testing.T) {
// First do a few dry-runs, making sure the number of addresses in the
// database us not inflated.
dryRunTx, err := w.txToOutputs(txOuts, 0, 1, 1000, true)
dryRunTx, err := w.txToOutputs(txOuts, nil, 0, 1, 1000, true)
if err != nil {
t.Fatalf("unable to author tx: %v", err)
}
@ -85,7 +86,7 @@ func TestTxToOutputsDryRun(t *testing.T) {
t.Fatalf("expected 1 address, found %v", len(addresses))
}
dryRunTx2, err := w.txToOutputs(txOuts, 0, 1, 1000, true)
dryRunTx2, err := w.txToOutputs(txOuts, nil, 0, 1, 1000, true)
if err != nil {
t.Fatalf("unable to author tx: %v", err)
}
@ -118,7 +119,7 @@ func TestTxToOutputsDryRun(t *testing.T) {
// Now we do a proper, non-dry run. This should add a change address
// to the database.
tx, err := w.txToOutputs(txOuts, 0, 1, 1000, false)
tx, err := w.txToOutputs(txOuts, nil, 0, 1, 1000, false)
if err != nil {
t.Fatalf("unable to author tx: %v", err)
}

View file

@ -12,6 +12,7 @@ import (
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/psbt"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet/txauthor"
"github.com/btcsuite/btcwallet/wallet/txrules"
"github.com/btcsuite/btcwallet/wtxmgr"
@ -24,17 +25,22 @@ import (
// is created and the index -1 is returned.
//
// NOTE: If the packet doesn't contain any inputs, coin selection is performed
// automatically. If the packet does contain any inputs, it is assumed that full
// coin selection happened externally and no additional inputs are added. If the
// specified inputs aren't enough to fund the outputs with the given fee rate,
// an error is returned.
// automatically, only selecting inputs from the account based on the given key
// scope and account number. If a key scope is not specified, then inputs from
// accounts matching the account number provided across all key scopes may be
// selected. This is done to handle the default account case, where a user wants
// to fund a PSBT with inputs regardless of their type (NP2WKH, P2WKH, etc.). If
// the packet does contain any inputs, it is assumed that full coin selection
// happened externally and no additional inputs are added. If the specified
// inputs aren't enough to fund the outputs with the given fee rate, an error is
// returned.
//
// NOTE: A caller of the method should hold the global coin selection lock of
// the wallet. However, no UTXO specific lock lease is acquired for any of the
// selected/validated inputs by this method. It is in the caller's
// responsibility to lock the inputs before handing the partial transaction out.
func (w *Wallet) FundPsbt(packet *psbt.Packet, account uint32,
feeSatPerKB btcutil.Amount) (int32, error) {
func (w *Wallet) FundPsbt(packet *psbt.Packet, keyScope *waddrmgr.KeyScope,
account uint32, feeSatPerKB btcutil.Amount) (int32, error) {
// Make sure the packet is well formed. We only require there to be at
// least one output but not necessarily any inputs.
@ -113,8 +119,8 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, account uint32,
// includes everything we need, specifically fee estimation and
// change address creation.
tx, err = w.CreateSimpleTx(
account, packet.UnsignedTx.TxOut, 1, feeSatPerKB,
false,
keyScope, account, packet.UnsignedTx.TxOut, 1,
feeSatPerKB, false,
)
if err != nil {
return 0, fmt.Errorf("error creating funding TX: %v",
@ -166,7 +172,9 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, account uint32,
if err != nil {
return 0, err
}
_, changeSource := w.addrMgrWithChangeSource(dbtx, account)
_, changeSource := w.addrMgrWithChangeSource(
dbtx, keyScope, account,
)
// Ask the txauthor to create a transaction with our selected
// coins. This will perform fee estimation and add a change
@ -232,9 +240,9 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, account uint32,
//
// NOTE: This method does NOT publish the transaction after it's been finalized
// successfully.
//
// TODO: require account and check if watch only to avoid signing.
func (w *Wallet) FinalizePsbt(packet *psbt.Packet) error {
func (w *Wallet) FinalizePsbt(keyScope *waddrmgr.KeyScope, account uint32,
packet *psbt.Packet) error {
// Let's check that this is actually something we can and want to sign.
// We need at least one input and one output.
err := psbt.VerifyInputOutputLen(packet, true, true)

View file

@ -219,7 +219,7 @@ func TestFundPsbt(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
changeIndex, err := w.FundPsbt(
tc.packet, 0, tc.feeRateSatPerKB,
tc.packet, nil, 0, tc.feeRateSatPerKB,
)
// In any case, unlock the UTXO before continuing, we
@ -391,7 +391,7 @@ func TestFinalizePsbt(t *testing.T) {
}
// Finalize it to add all witness data then extract the final TX.
err = w.FinalizePsbt(packet)
err = w.FinalizePsbt(nil, 0, packet)
if err != nil {
t.Fatalf("error finalizing PSBT packet: %v", err)
}

View file

@ -59,12 +59,14 @@ func TestFetchInputInfo(t *testing.T) {
t.Fatalf("expected derivation path of length %v, got %v", 3,
len(derivationPath.Bip32Path))
}
if derivationPath.Bip32Path[0] != waddrmgr.KeyScopeBIP0084.Purpose {
if derivationPath.Bip32Path[0] !=
waddrmgr.KeyScopeBIP0084.Purpose+hdkeychain.HardenedKeyStart {
t.Fatalf("expected purpose %v, got %v",
waddrmgr.KeyScopeBIP0084.Purpose,
derivationPath.Bip32Path[0])
}
if derivationPath.Bip32Path[1] != waddrmgr.KeyScopeBIP0084.Coin {
if derivationPath.Bip32Path[1] !=
waddrmgr.KeyScopeBIP0084.Coin+hdkeychain.HardenedKeyStart {
t.Fatalf("expected coin type %v, got %v",
waddrmgr.KeyScopeBIP0084.Coin,
derivationPath.Bip32Path[1])

View file

@ -1125,6 +1125,7 @@ func logFilterBlocksResp(block wtxmgr.BlockMeta,
type (
createTxRequest struct {
keyScope *waddrmgr.KeyScope
account uint32
outputs []*wire.TxOut
minconf int32
@ -1159,8 +1160,10 @@ out:
txr.resp <- createTxResponse{nil, err}
continue
}
tx, err := w.txToOutputs(txr.outputs, txr.account,
txr.minconf, txr.feeSatPerKB, txr.dryRun)
tx, err := w.txToOutputs(
txr.outputs, txr.keyScope, txr.account,
txr.minconf, txr.feeSatPerKB, txr.dryRun,
)
heldUnlock.release()
txr.resp <- createTxResponse{tx, err}
case <-quit:
@ -1170,20 +1173,25 @@ out:
w.wg.Done()
}
// CreateSimpleTx creates a new signed transaction spending unspent P2PKH
// outputs with at least minconf confirmations spending to any number of
// address/amount pairs. Change and an appropriate transaction fee are
// automatically included, if necessary. All transaction creation through this
// function is serialized to prevent the creation of many transactions which
// spend the same outputs.
// CreateSimpleTx creates a new signed transaction spending unspent outputs with
// at least minconf confirmations spending to any number of address/amount
// pairs. Only unspent outputs belonging to the given key scope and account will
// be selected, unless a key scope is not specified. In that case, inputs from all
// accounts may be selected, no matter what key scope they belong to. This is
// done to handle the default account case, where a user wants to fund a PSBT
// with inputs regardless of their type (NP2WKH, P2WKH, etc.). Change and an
// appropriate transaction fee are automatically included, if necessary. All
// transaction creation through this function is serialized to prevent the
// creation of many transactions which spend the same outputs.
//
// NOTE: The dryRun argument can be set true to create a tx that doesn't alter
// the database. A tx created with this set to true SHOULD NOT be broadcasted.
func (w *Wallet) CreateSimpleTx(account uint32, outputs []*wire.TxOut,
minconf int32, satPerKb btcutil.Amount, dryRun bool) (
*txauthor.AuthoredTx, error) {
func (w *Wallet) CreateSimpleTx(keyScope *waddrmgr.KeyScope, account uint32,
outputs []*wire.TxOut, minconf int32, satPerKb btcutil.Amount,
dryRun bool) (*txauthor.AuthoredTx, error) {
req := createTxRequest{
keyScope: keyScope,
account: account,
outputs: outputs,
minconf: minconf,
@ -3104,10 +3112,16 @@ func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcu
return amount, err
}
// SendOutputs creates and sends payment transactions. It returns the
// transaction upon success.
func (w *Wallet) SendOutputs(outputs []*wire.TxOut, account uint32,
minconf int32, satPerKb btcutil.Amount, label string) (*wire.MsgTx, error) {
// SendOutputs creates and sends payment transactions. Coin selection is
// performed by the wallet, choosing inputs that belong to the given key scope
// and account, unless a key scope is not specified. In that case, inputs from
// accounts matching the account number provided across all key scopes may be
// selected. This is done to handle the default account case, where a user wants
// to fund a PSBT with inputs regardless of their type (NP2WKH, P2WKH, etc.). It
// returns the transaction upon success.
func (w *Wallet) SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
account uint32, minconf int32, satPerKb btcutil.Amount,
label string) (*wire.MsgTx, error) {
// Ensure the outputs to be created adhere to the network's consensus
// rules.
@ -3125,7 +3139,7 @@ func (w *Wallet) SendOutputs(outputs []*wire.TxOut, account uint32,
// continue to re-broadcast the transaction upon restarts until it has
// been confirmed.
createdTx, err := w.CreateSimpleTx(
account, outputs, minconf, satPerKb, false,
keyScope, account, outputs, minconf, satPerKb, false,
)
if err != nil {
return nil, err