Skip to content

Commit 0425ce5

Browse files
committed
Merge bitcoin/bitcoin#25679: wallet: Correctly identify external inputs that are also in the wallet
ef8e2a5 tests: Test that external inputs of txs in wallet is handled correctly (Andrew Chow) eb87963 wallet: Try estimating input size with external data if wallet fails (Andrew Chow) a537d7a wallet: SelectExternal actually external inputs (Andrew Chow) f2d00bf wallet: Add CWallet::IsMine(COutPoint) (Andrew Chow) Pull request description: if a transaction is being funded that has an external input, and that input's parent is also in the wallet, we will fail to detect that and fail to fund the transaction. In order to correctly detect such inputs, we need to be doing `IsMine` on all specified inputs in order to use `Select` and `SelectExternal` correctly. Additionally `SelectCoins` needs to call `CalculateMaximumSignedInputSize` with the correct parameters which depends on whether the wallet is able to solve for the input. Because there are some situations where the wallet could find an external input to belong to it (e.g. watching an address - unable to solve, but will be ISMINE_WATCHONLY), instead of switching which `CalculateMaximumSignedInputSize` to use, we should call the one that uses the wallet, and if that fails, try again with the one that uses external solving data. Also adds a test for this case. ACKs for top commit: instagibbs: ACK bitcoin/bitcoin@ef8e2a5 furszy: ACK ef8e2a5 ishaanam: reACK ef8e2a5 Tree-SHA512: a43c4aefeed4605f33a36ce87ebb916e2c153fea6d415b02c9a89275e84a7e3bf12840b33c296d2d2bde46350390da48d9262f9567338e3f21d5936aae4caa1e
2 parents 888628c + ef8e2a5 commit 0425ce5

File tree

4 files changed

+44
-11
lines changed

4 files changed

+44
-11
lines changed

src/wallet/spend.cpp

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -569,8 +569,12 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& a
569569
if (!coin_control.GetExternalOutput(outpoint, txout)) {
570570
return std::nullopt;
571571
}
572+
}
573+
574+
if (input_bytes == -1) {
572575
input_bytes = CalculateMaximumSignedInputSize(txout, outpoint, &coin_control.m_external_provider, &coin_control);
573576
}
577+
574578
// If available, override calculated size with coin control specified size
575579
if (coin_control.HasInputWeight(outpoint)) {
576580
input_bytes = GetVirtualTransactionSize(coin_control.GetInputWeight(outpoint), 0, 0);
@@ -1127,12 +1131,16 @@ bool FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& nFeeRet,
11271131
wallet.chain().findCoins(coins);
11281132

11291133
for (const CTxIn& txin : tx.vin) {
1130-
// if it's not in the wallet and corresponding UTXO is found than select as external output
11311134
const auto& outPoint = txin.prevout;
1132-
if (wallet.mapWallet.find(outPoint.hash) == wallet.mapWallet.end() && !coins[outPoint].out.IsNull()) {
1133-
coinControl.SelectExternal(outPoint, coins[outPoint].out);
1134-
} else {
1135+
if (wallet.IsMine(outPoint)) {
1136+
// The input was found in the wallet, so select as internal
11351137
coinControl.Select(outPoint);
1138+
} else if (coins[outPoint].out.IsNull()) {
1139+
error = _("Unable to find UTXO for external input");
1140+
return false;
1141+
} else {
1142+
// The input was not in the wallet, but is in the UTXO set, so select as external
1143+
coinControl.SelectExternal(outPoint, coins[outPoint].out);
11361144
}
11371145
}
11381146

src/wallet/wallet.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,6 +1428,19 @@ bool CWallet::IsMine(const CTransaction& tx) const
14281428
return false;
14291429
}
14301430

1431+
isminetype CWallet::IsMine(const COutPoint& outpoint) const
1432+
{
1433+
AssertLockHeld(cs_wallet);
1434+
auto wtx = GetWalletTx(outpoint.hash);
1435+
if (!wtx) {
1436+
return ISMINE_NO;
1437+
}
1438+
if (outpoint.n >= wtx->tx->vout.size()) {
1439+
return ISMINE_NO;
1440+
}
1441+
return IsMine(wtx->tx->vout[outpoint.n]);
1442+
}
1443+
14311444
bool CWallet::IsFromMe(const CTransaction& tx) const
14321445
{
14331446
return (GetDebit(tx, ISMINE_ALL) > 0);

src/wallet/wallet.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
685685
CAmount GetDebit(const CTxIn& txin, const isminefilter& filter) const;
686686
isminetype IsMine(const CTxOut& txout) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
687687
bool IsMine(const CTransaction& tx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
688+
isminetype IsMine(const COutPoint& outpoint) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
688689
/** should probably be renamed to IsRelevantToMe */
689690
bool IsFromMe(const CTransaction& tx) const;
690691
CAmount GetDebit(const CTransaction& tx, const isminefilter& filter) const;

test/functional/rpc_fundrawtransaction.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ def test_invalid_input(self):
408408
inputs = [ {'txid' : "1c7f966dab21119bac53213a2bc7532bff1fa844c124fd750a7d0b1332440bd1", 'vout' : 0} ] #invalid vin!
409409
outputs = { self.nodes[0].getnewaddress() : 1.0}
410410
rawtx = self.nodes[2].createrawtransaction(inputs, outputs)
411-
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawtx)
411+
assert_raises_rpc_error(-4, "Unable to find UTXO for external input", self.nodes[2].fundrawtransaction, rawtx)
412412

413413
def test_fee_p2pkh(self):
414414
"""Compare fee of a standard pubkeyhash transaction."""
@@ -1079,17 +1079,28 @@ def test_weight_calculation(self):
10791079
self.nodes[2].createwallet("test_weight_calculation")
10801080
wallet = self.nodes[2].get_wallet_rpc("test_weight_calculation")
10811081

1082-
addr = wallet.getnewaddress()
1083-
txid = self.nodes[0].sendtoaddress(addr, 5)
1082+
addr = wallet.getnewaddress(address_type="bech32")
1083+
ext_addr = self.nodes[0].getnewaddress(address_type="bech32")
1084+
txid = self.nodes[0].send([{addr: 5}, {ext_addr: 5}])["txid"]
10841085
vout = find_vout_for_address(self.nodes[0], txid, addr)
1086+
ext_vout = find_vout_for_address(self.nodes[0], txid, ext_addr)
10851087

1086-
self.nodes[0].sendtoaddress(wallet.getnewaddress(), 5)
1088+
self.nodes[0].sendtoaddress(wallet.getnewaddress(address_type="bech32"), 5)
10871089
self.generate(self.nodes[0], 1)
10881090

1089-
rawtx = wallet.createrawtransaction([{'txid': txid, 'vout': vout}], [{self.nodes[0].getnewaddress(): 9.999}])
1090-
fundedtx = wallet.fundrawtransaction(rawtx, {'fee_rate': 10})
1091+
rawtx = wallet.createrawtransaction([{'txid': txid, 'vout': vout}], [{self.nodes[0].getnewaddress(address_type="bech32"): 8}])
1092+
fundedtx = wallet.fundrawtransaction(rawtx, {'fee_rate': 10, "change_type": "bech32"})
10911093
# with 71-byte signatures we should expect following tx size
1092-
tx_size = 10 + 41*2 + 31*2 + (2 + 107*2)/4
1094+
# tx overhead (10) + 2 inputs (41 each) + 2 p2wpkh (31 each) + (segwit marker and flag (2) + 2 p2wpkh 71 byte sig witnesses (107 each)) / witness scaling factor (4)
1095+
tx_size = ceil(10 + 41*2 + 31*2 + (2 + 107*2)/4)
1096+
assert_equal(fundedtx['fee'] * COIN, tx_size * 10)
1097+
1098+
# Using the other output should have 72 byte sigs
1099+
rawtx = wallet.createrawtransaction([{'txid': txid, 'vout': ext_vout}], [{self.nodes[0].getnewaddress(): 13}])
1100+
ext_desc = self.nodes[0].getaddressinfo(ext_addr)["desc"]
1101+
fundedtx = wallet.fundrawtransaction(rawtx, {'fee_rate': 10, "change_type": "bech32", "solving_data": {"descriptors": [ext_desc]}})
1102+
# tx overhead (10) + 3 inputs (41 each) + 2 p2wpkh(31 each) + (segwit marker and flag (2) + 2 p2wpkh 71 bytes sig witnesses (107 each) + p2wpkh 72 byte sig witness (108)) / witness scaling factor (4)
1103+
tx_size = ceil(10 + 41*3 + 31*2 + (2 + 107*2 + 108)/4)
10931104
assert_equal(fundedtx['fee'] * COIN, tx_size * 10)
10941105

10951106
self.nodes[2].unloadwallet("test_weight_calculation")

0 commit comments

Comments
 (0)