Merge bitcoin/bitcoin#17211: Allow fundrawtransaction and walletcreatefundedpsbt to take external inputs

928af61cdb allow send rpc take external inputs and solving data (Andrew Chow)
e39b5a5e7a Tests for funding with external inputs (Andrew Chow)
38f5642ccc allow fundtx rpcs to work with external inputs (Andrew Chow)
d5cfb864ae Allow Coin Selection be able to take external inputs (Andrew Chow)
a00eb388e8 Allow CInputCoin to also be constructed with COutPoint and CTxOut (Andrew Chow)

Pull request description:

  Currently `fundrawtransaction` and `walletcreatefundedpsbt` both do not allow external inputs as the wallet does not have the information necessary to estimate their fees.

  This PR adds an additional argument to both those RPCs which allows the user to specify solving data. This way, the wallet can use that solving data to estimate the size of those inputs. The solving data can be public keys, scripts, or descriptors.

ACKs for top commit:
  prayank23:
    reACK 928af61cdb
  meshcollider:
    Re-utACK 928af61cdb
  instagibbs:
    crACK 928af61cdb
  yanmaani:
    utACK 928af61.

Tree-SHA512: bc7a6ef8961a7f4971ea5985d75e2d6dc50c2a90b44c664a1c4b0f1be5c1c97823516358fdaab35771a4701dbefc0862127b1d0d4bfd02b4f20d2befa4434700
This commit is contained in:
Samuel Dobson 2021-10-04 21:46:51 +13:00
commit 573b4621cc
No known key found for this signature in database
GPG key ID: D300116E1C875A3D
10 changed files with 383 additions and 52 deletions

View file

@ -9,9 +9,14 @@
#include <policy/feerate.h> #include <policy/feerate.h>
#include <policy/fees.h> #include <policy/fees.h>
#include <primitives/transaction.h> #include <primitives/transaction.h>
#include <script/keyorigin.h>
#include <script/signingprovider.h>
#include <script/standard.h> #include <script/standard.h>
#include <optional> #include <optional>
#include <algorithm>
#include <map>
#include <set>
const int DEFAULT_MIN_DEPTH = 0; const int DEFAULT_MIN_DEPTH = 0;
const int DEFAULT_MAX_DEPTH = 9999999; const int DEFAULT_MAX_DEPTH = 9999999;
@ -53,6 +58,8 @@ public:
int m_min_depth = DEFAULT_MIN_DEPTH; int m_min_depth = DEFAULT_MIN_DEPTH;
//! Maximum chain depth value for coin availability //! Maximum chain depth value for coin availability
int m_max_depth = DEFAULT_MAX_DEPTH; int m_max_depth = DEFAULT_MAX_DEPTH;
//! SigningProvider that has pubkeys and scripts to do spend size estimation for external inputs
FlatSigningProvider m_external_provider;
CCoinControl(); CCoinControl();
@ -66,11 +73,32 @@ public:
return (setSelected.count(output) > 0); return (setSelected.count(output) > 0);
} }
bool IsExternalSelected(const COutPoint& output) const
{
return (m_external_txouts.count(output) > 0);
}
bool GetExternalOutput(const COutPoint& outpoint, CTxOut& txout) const
{
const auto ext_it = m_external_txouts.find(outpoint);
if (ext_it == m_external_txouts.end()) {
return false;
}
txout = ext_it->second;
return true;
}
void Select(const COutPoint& output) void Select(const COutPoint& output)
{ {
setSelected.insert(output); setSelected.insert(output);
} }
void Select(const COutPoint& outpoint, const CTxOut& txout)
{
setSelected.insert(outpoint);
m_external_txouts.emplace(outpoint, txout);
}
void UnSelect(const COutPoint& output) void UnSelect(const COutPoint& output)
{ {
setSelected.erase(output); setSelected.erase(output);
@ -88,6 +116,7 @@ public:
private: private:
std::set<COutPoint> setSelected; std::set<COutPoint> setSelected;
std::map<COutPoint, CTxOut> m_external_txouts;
}; };
#endif // BITCOIN_WALLET_COINCONTROL_H #endif // BITCOIN_WALLET_COINCONTROL_H

View file

@ -37,6 +37,18 @@ public:
m_input_bytes = input_bytes; m_input_bytes = input_bytes;
} }
CInputCoin(const COutPoint& outpoint_in, const CTxOut& txout_in)
{
outpoint = outpoint_in;
txout = txout_in;
effective_value = txout.nValue;
}
CInputCoin(const COutPoint& outpoint_in, const CTxOut& txout_in, int input_bytes) : CInputCoin(outpoint_in, txout_in)
{
m_input_bytes = input_bytes;
}
COutPoint outpoint; COutPoint outpoint;
CTxOut txout; CTxOut txout;
CAmount effective_value; CAmount effective_value;

View file

@ -43,6 +43,7 @@
#include <univalue.h> #include <univalue.h>
#include <map>
using interfaces::FoundBlock; using interfaces::FoundBlock;
@ -3213,6 +3214,7 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out,
{"fee_rate", UniValueType()}, // will be checked by AmountFromValue() in SetFeeEstimateMode() {"fee_rate", UniValueType()}, // will be checked by AmountFromValue() in SetFeeEstimateMode()
{"feeRate", UniValueType()}, // will be checked by AmountFromValue() below {"feeRate", UniValueType()}, // will be checked by AmountFromValue() below
{"psbt", UniValueType(UniValue::VBOOL)}, {"psbt", UniValueType(UniValue::VBOOL)},
{"solving_data", UniValueType(UniValue::VOBJ)},
{"subtractFeeFromOutputs", UniValueType(UniValue::VARR)}, {"subtractFeeFromOutputs", UniValueType(UniValue::VARR)},
{"subtract_fee_from_outputs", UniValueType(UniValue::VARR)}, {"subtract_fee_from_outputs", UniValueType(UniValue::VARR)},
{"replaceable", UniValueType(UniValue::VBOOL)}, {"replaceable", UniValueType(UniValue::VBOOL)},
@ -3289,6 +3291,54 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out,
coinControl.fAllowWatchOnly = ParseIncludeWatchonly(NullUniValue, wallet); coinControl.fAllowWatchOnly = ParseIncludeWatchonly(NullUniValue, wallet);
} }
if (options.exists("solving_data")) {
UniValue solving_data = options["solving_data"].get_obj();
if (solving_data.exists("pubkeys")) {
for (const UniValue& pk_univ : solving_data["pubkeys"].get_array().getValues()) {
const std::string& pk_str = pk_univ.get_str();
if (!IsHex(pk_str)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("'%s' is not hex", pk_str));
}
const std::vector<unsigned char> data(ParseHex(pk_str));
CPubKey pubkey(data.begin(), data.end());
if (!pubkey.IsFullyValid()) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("'%s' is not a valid public key", pk_str));
}
coinControl.m_external_provider.pubkeys.emplace(pubkey.GetID(), pubkey);
// Add witness script for pubkeys
const CScript wit_script = GetScriptForDestination(WitnessV0KeyHash(pubkey));
coinControl.m_external_provider.scripts.emplace(CScriptID(wit_script), wit_script);
}
}
if (solving_data.exists("scripts")) {
for (const UniValue& script_univ : solving_data["scripts"].get_array().getValues()) {
const std::string& script_str = script_univ.get_str();
if (!IsHex(script_str)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("'%s' is not hex", script_str));
}
std::vector<unsigned char> script_data(ParseHex(script_str));
const CScript script(script_data.begin(), script_data.end());
coinControl.m_external_provider.scripts.emplace(CScriptID(script), script);
}
}
if (solving_data.exists("descriptors")) {
for (const UniValue& desc_univ : solving_data["descriptors"].get_array().getValues()) {
const std::string& desc_str = desc_univ.get_str();
FlatSigningProvider desc_out;
std::string error;
std::vector<CScript> scripts_temp;
std::unique_ptr<Descriptor> desc = Parse(desc_str, desc_out, error, true);
if (!desc) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Unable to parse descriptor '%s': %s", desc_str, error));
}
desc->Expand(0, desc_out, scripts_temp, desc_out);
coinControl.m_external_provider = Merge(coinControl.m_external_provider, desc_out);
}
}
}
if (tx.vout.size() == 0) if (tx.vout.size() == 0)
throw JSONRPCError(RPC_INVALID_PARAMETER, "TX must have at least one output"); throw JSONRPCError(RPC_INVALID_PARAMETER, "TX must have at least one output");
@ -3306,6 +3356,19 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out,
setSubtractFeeFromOutputs.insert(pos); setSubtractFeeFromOutputs.insert(pos);
} }
// Fetch specified UTXOs from the UTXO set to get the scriptPubKeys and values of the outputs being selected
// and to match with the given solving_data. Only used for non-wallet outputs.
std::map<COutPoint, Coin> coins;
for (const CTxIn& txin : tx.vin) {
coins[txin.prevout]; // Create empty map entry keyed by prevout.
}
wallet.chain().findCoins(coins);
for (const auto& coin : coins) {
if (!coin.second.out.IsNull()) {
coinControl.Select(coin.first, coin.second.out);
}
}
bilingual_str error; bilingual_str error;
if (!FundTransaction(wallet, tx, fee_out, change_position, error, lockUnspents, setSubtractFeeFromOutputs, coinControl)) { if (!FundTransaction(wallet, tx, fee_out, change_position, error, lockUnspents, setSubtractFeeFromOutputs, coinControl)) {
@ -3321,8 +3384,9 @@ static RPCHelpMan fundrawtransaction()
"No existing outputs will be modified unless \"subtractFeeFromOutputs\" is specified.\n" "No existing outputs will be modified unless \"subtractFeeFromOutputs\" is specified.\n"
"Note that inputs which were signed may need to be resigned after completion since in/outputs have been added.\n" "Note that inputs which were signed may need to be resigned after completion since in/outputs have been added.\n"
"The inputs added will not be signed, use signrawtransactionwithkey\n" "The inputs added will not be signed, use signrawtransactionwithkey\n"
" or signrawtransactionwithwallet for that.\n" "or signrawtransactionwithwallet for that.\n"
"Note that all existing inputs must have their previous output transaction be in the wallet.\n" "All existing inputs must either have their previous output transaction be in the wallet\n"
"or be in the UTXO set. Solving data must be provided for non-wallet inputs.\n"
"Note that all inputs selected must be of standard form and P2SH scripts must be\n" "Note that all inputs selected must be of standard form and P2SH scripts must be\n"
"in the wallet using importaddress or addmultisigaddress (to calculate fees).\n" "in the wallet using importaddress or addmultisigaddress (to calculate fees).\n"
"You can see whether this is the case by checking the \"solvable\" field in the listunspent output.\n" "You can see whether this is the case by checking the \"solvable\" field in the listunspent output.\n"
@ -3357,6 +3421,26 @@ static RPCHelpMan fundrawtransaction()
{"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks"}, {"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks"},
{"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"unset"}, std::string() + "The fee estimate mode, must be one of (case insensitive):\n" {"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"unset"}, std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
" \"" + FeeModes("\"\n\"") + "\""}, " \"" + FeeModes("\"\n\"") + "\""},
{"solving_data", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "Keys and scripts needed for producing a final transaction with a dummy signature.\n"
"Used for fee estimation during coin selection.",
{
{"pubkeys", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Public keys involved in this transaction.",
{
{"pubkey", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A public key"},
},
},
{"scripts", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Scripts involved in this transaction.",
{
{"script", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A script"},
},
},
{"descriptors", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Descriptors that provide solving data for this transaction.",
{
{"descriptor", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A descriptor"},
},
}
}
},
}, },
"options"}, "options"},
{"iswitness", RPCArg::Type::BOOL, RPCArg::DefaultHint{"depends on heuristic tests"}, "Whether the transaction hex is a serialized witness transaction.\n" {"iswitness", RPCArg::Type::BOOL, RPCArg::DefaultHint{"depends on heuristic tests"}, "Whether the transaction hex is a serialized witness transaction.\n"
@ -4202,6 +4286,26 @@ static RPCHelpMan send()
}, },
{"replaceable", RPCArg::Type::BOOL, RPCArg::DefaultHint{"wallet default"}, "Marks this transaction as BIP125 replaceable.\n" {"replaceable", RPCArg::Type::BOOL, RPCArg::DefaultHint{"wallet default"}, "Marks this transaction as BIP125 replaceable.\n"
"Allows this transaction to be replaced by a transaction with higher fees"}, "Allows this transaction to be replaced by a transaction with higher fees"},
{"solving_data", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "Keys and scripts needed for producing a final transaction with a dummy signature.\n"
"Used for fee estimation during coin selection.",
{
{"pubkeys", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Public keys involved in this transaction.",
{
{"pubkey", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A public key"},
},
},
{"scripts", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Scripts involved in this transaction.",
{
{"script", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A script"},
},
},
{"descriptors", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Descriptors that provide solving data for this transaction.",
{
{"descriptor", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A descriptor"},
},
}
}
},
}, },
"options"}, "options"},
}, },
@ -4489,7 +4593,9 @@ static RPCHelpMan walletcreatefundedpsbt()
{ {
return RPCHelpMan{"walletcreatefundedpsbt", return RPCHelpMan{"walletcreatefundedpsbt",
"\nCreates and funds a transaction in the Partially Signed Transaction format.\n" "\nCreates and funds a transaction in the Partially Signed Transaction format.\n"
"Implements the Creator and Updater roles.\n", "Implements the Creator and Updater roles.\n"
"All existing inputs must either have their previous output transaction be in the wallet\n"
"or be in the UTXO set. Solving data must be provided for non-wallet inputs.\n",
{ {
{"inputs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, "Leave empty to add inputs automatically. See add_inputs option.", {"inputs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, "Leave empty to add inputs automatically. See add_inputs option.",
{ {
@ -4546,6 +4652,26 @@ static RPCHelpMan walletcreatefundedpsbt()
{"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks"}, {"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks"},
{"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"unset"}, std::string() + "The fee estimate mode, must be one of (case insensitive):\n" {"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"unset"}, std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
" \"" + FeeModes("\"\n\"") + "\""}, " \"" + FeeModes("\"\n\"") + "\""},
{"solving_data", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "Keys and scripts needed for producing a final transaction with a dummy signature.\n"
"Used for fee estimation during coin selection.",
{
{"pubkeys", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Public keys involved in this transaction.",
{
{"pubkey", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A public key"},
},
},
{"scripts", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Scripts involved in this transaction.",
{
{"script", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A script"},
},
},
{"descriptors", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Descriptors that provide solving data for this transaction.",
{
{"descriptor", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A descriptor"},
},
}
}
},
}, },
"options"}, "options"},
{"bip32derivs", RPCArg::Type::BOOL, RPCArg::Default{true}, "Include BIP 32 derivation paths for public keys if we know them"}, {"bip32derivs", RPCArg::Type::BOOL, RPCArg::Default{true}, "Include BIP 32 derivation paths for public keys if we know them"},

View file

@ -5,6 +5,7 @@
#include <consensus/validation.h> #include <consensus/validation.h>
#include <interfaces/chain.h> #include <interfaces/chain.h>
#include <policy/policy.h> #include <policy/policy.h>
#include <script/signingprovider.h>
#include <util/check.h> #include <util/check.h>
#include <util/fees.h> #include <util/fees.h>
#include <util/moneystr.h> #include <util/moneystr.h>
@ -31,21 +32,27 @@ std::string COutput::ToString() const
return strprintf("COutput(%s, %d, %d) [%s]", tx->GetHash().ToString(), i, nDepth, FormatMoney(tx->tx->vout[i].nValue)); return strprintf("COutput(%s, %d, %d) [%s]", tx->GetHash().ToString(), i, nDepth, FormatMoney(tx->tx->vout[i].nValue));
} }
int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* wallet, bool use_max_sig) int CalculateMaximumSignedInputSize(const CTxOut& txout, const SigningProvider* provider, bool use_max_sig)
{ {
CMutableTransaction txn; CMutableTransaction txn;
txn.vin.push_back(CTxIn(COutPoint())); txn.vin.push_back(CTxIn(COutPoint()));
if (!wallet->DummySignInput(txn.vin[0], txout, use_max_sig)) { if (!provider || !DummySignInput(*provider, txn.vin[0], txout, use_max_sig)) {
return -1; return -1;
} }
return GetVirtualTransactionInputSize(txn.vin[0]); return GetVirtualTransactionInputSize(txn.vin[0]);
} }
int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* wallet, bool use_max_sig)
{
const std::unique_ptr<SigningProvider> provider = wallet->GetSolvingProvider(txout.scriptPubKey);
return CalculateMaximumSignedInputSize(txout, provider.get(), use_max_sig);
}
// txouts needs to be in the order of tx.vin // txouts needs to be in the order of tx.vin
TxSize CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, const std::vector<CTxOut>& txouts, bool use_max_sig) TxSize CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, const std::vector<CTxOut>& txouts, const CCoinControl* coin_control)
{ {
CMutableTransaction txNew(tx); CMutableTransaction txNew(tx);
if (!wallet->DummySignTx(txNew, txouts, use_max_sig)) { if (!wallet->DummySignTx(txNew, txouts, coin_control)) {
return TxSize{-1, -1}; return TxSize{-1, -1};
} }
CTransaction ctx(txNew); CTransaction ctx(txNew);
@ -54,19 +61,27 @@ TxSize CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *walle
return TxSize{vsize, weight}; return TxSize{vsize, weight};
} }
TxSize CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, bool use_max_sig) TxSize CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, const CCoinControl* coin_control)
{ {
std::vector<CTxOut> txouts; std::vector<CTxOut> txouts;
// Look up the inputs. The inputs are either in the wallet, or in coin_control.
for (const CTxIn& input : tx.vin) { for (const CTxIn& input : tx.vin) {
const auto mi = wallet->mapWallet.find(input.prevout.hash); const auto mi = wallet->mapWallet.find(input.prevout.hash);
// Can not estimate size without knowing the input details // Can not estimate size without knowing the input details
if (mi == wallet->mapWallet.end()) { if (mi != wallet->mapWallet.end()) {
assert(input.prevout.n < mi->second.tx->vout.size());
txouts.emplace_back(mi->second.tx->vout.at(input.prevout.n));
} else if (coin_control) {
CTxOut txout;
if (!coin_control->GetExternalOutput(input.prevout, txout)) {
return TxSize{-1, -1}; return TxSize{-1, -1};
} }
assert(input.prevout.n < mi->second.tx->vout.size()); txouts.emplace_back(txout);
txouts.emplace_back(mi->second.tx->vout[input.prevout.n]); } else {
return TxSize{-1, -1};
} }
return CalculateMaximumSignedTxSize(tx, wallet, txouts, use_max_sig); }
return CalculateMaximumSignedTxSize(tx, wallet, txouts, coin_control);
} }
void AvailableCoins(const CWallet& wallet, std::vector<COutput>& vCoins, const CCoinControl* coinControl, const CAmount& nMinimumAmount, const CAmount& nMaximumAmount, const CAmount& nMinimumSumAmount, const uint64_t nMaximumCount) void AvailableCoins(const CWallet& wallet, std::vector<COutput>& vCoins, const CCoinControl* coinControl, const CAmount& nMinimumAmount, const CAmount& nMaximumAmount, const CAmount& nMinimumSumAmount, const uint64_t nMaximumCount)
@ -435,18 +450,29 @@ bool SelectCoins(const CWallet& wallet, const std::vector<COutput>& vAvailableCo
std::vector<COutPoint> vPresetInputs; std::vector<COutPoint> vPresetInputs;
coin_control.ListSelected(vPresetInputs); coin_control.ListSelected(vPresetInputs);
for (const COutPoint& outpoint : vPresetInputs) for (const COutPoint& outpoint : vPresetInputs) {
{ int input_bytes = -1;
CTxOut txout;
std::map<uint256, CWalletTx>::const_iterator it = wallet.mapWallet.find(outpoint.hash); std::map<uint256, CWalletTx>::const_iterator it = wallet.mapWallet.find(outpoint.hash);
if (it != wallet.mapWallet.end()) if (it != wallet.mapWallet.end()) {
{
const CWalletTx& wtx = it->second; const CWalletTx& wtx = it->second;
// Clearly invalid input, fail // Clearly invalid input, fail
if (wtx.tx->vout.size() <= outpoint.n) { if (wtx.tx->vout.size() <= outpoint.n) {
return false; return false;
} }
// Just to calculate the marginal byte size input_bytes = GetTxSpendSize(wallet, wtx, outpoint.n, false);
CInputCoin coin(wtx.tx, outpoint.n, GetTxSpendSize(wallet, wtx, outpoint.n, false)); txout = wtx.tx->vout.at(outpoint.n);
}
if (input_bytes == -1) {
// The input is external. We either did not find the tx in mapWallet, or we did but couldn't compute the input size with wallet data
if (!coin_control.GetExternalOutput(outpoint, txout)) {
// Not ours, and we don't have solving data.
return false;
}
input_bytes = CalculateMaximumSignedInputSize(txout, &coin_control.m_external_provider, /* use_max_sig */ true);
}
CInputCoin coin(outpoint, txout, input_bytes);
nValueFromPresetInputs += coin.txout.nValue; nValueFromPresetInputs += coin.txout.nValue;
if (coin.m_input_bytes <= 0) { if (coin.m_input_bytes <= 0) {
return false; // Not solvable, can't estimate size for fee return false; // Not solvable, can't estimate size for fee
@ -458,9 +484,6 @@ bool SelectCoins(const CWallet& wallet, const std::vector<COutput>& vAvailableCo
value_to_select -= coin.effective_value; value_to_select -= coin.effective_value;
} }
setPresetCoins.insert(coin); setPresetCoins.insert(coin);
} else {
return false; // TODO: Allow non-wallet inputs
}
} }
// remove preset inputs from vCoins so that Coin Selection doesn't pick them. // remove preset inputs from vCoins so that Coin Selection doesn't pick them.
@ -788,10 +811,10 @@ static bool CreateTransactionInternal(
} }
// Calculate the transaction fee // Calculate the transaction fee
TxSize tx_sizes = CalculateMaximumSignedTxSize(CTransaction(txNew), &wallet, coin_control.fAllowWatchOnly); TxSize tx_sizes = CalculateMaximumSignedTxSize(CTransaction(txNew), &wallet, &coin_control);
int nBytes = tx_sizes.vsize; int nBytes = tx_sizes.vsize;
if (nBytes < 0) { if (nBytes < 0) {
error = _("Signing transaction failed"); error = _("Missing solving data for estimating transaction size");
return false; return false;
} }
nFeeRet = coin_selection_params.m_effective_feerate.GetFee(nBytes); nFeeRet = coin_selection_params.m_effective_feerate.GetFee(nBytes);
@ -813,7 +836,7 @@ static bool CreateTransactionInternal(
txNew.vout.erase(change_position); txNew.vout.erase(change_position);
// Because we have dropped this change, the tx size and required fee will be different, so let's recalculate those // Because we have dropped this change, the tx size and required fee will be different, so let's recalculate those
tx_sizes = CalculateMaximumSignedTxSize(CTransaction(txNew), &wallet, coin_control.fAllowWatchOnly); tx_sizes = CalculateMaximumSignedTxSize(CTransaction(txNew), &wallet, &coin_control);
nBytes = tx_sizes.vsize; nBytes = tx_sizes.vsize;
fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes); fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes);
} }

View file

@ -66,6 +66,7 @@ public:
//Get the marginal bytes of spending the specified output //Get the marginal bytes of spending the specified output
int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* pwallet, bool use_max_sig = false); int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* pwallet, bool use_max_sig = false);
int CalculateMaximumSignedInputSize(const CTxOut& txout, const SigningProvider* pwallet, bool use_max_sig = false);
struct TxSize { struct TxSize {
int64_t vsize{-1}; int64_t vsize{-1};
@ -76,8 +77,8 @@ struct TxSize {
* Use DummySignatureCreator, which inserts 71 byte signatures everywhere. * Use DummySignatureCreator, which inserts 71 byte signatures everywhere.
* NOTE: this requires that all inputs must be in mapWallet (eg the tx should * NOTE: this requires that all inputs must be in mapWallet (eg the tx should
* be AllInputsMine). */ * be AllInputsMine). */
TxSize CalculateMaximumSignedTxSize(const CTransaction& tx, const CWallet* wallet, const std::vector<CTxOut>& txouts, bool use_max_sig = false); TxSize CalculateMaximumSignedTxSize(const CTransaction& tx, const CWallet* wallet, const std::vector<CTxOut>& txouts, const CCoinControl* coin_control = nullptr);
TxSize CalculateMaximumSignedTxSize(const CTransaction& tx, const CWallet* wallet, bool use_max_sig = false) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet); TxSize CalculateMaximumSignedTxSize(const CTransaction& tx, const CWallet* wallet, const CCoinControl* coin_control = nullptr) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet);
/** /**
* populate vCoins with vector of available COutputs. * populate vCoins with vector of available COutputs.

View file

@ -1448,19 +1448,13 @@ bool CWallet::AddWalletFlags(uint64_t flags)
// Helper for producing a max-sized low-S low-R signature (eg 71 bytes) // Helper for producing a max-sized low-S low-R signature (eg 71 bytes)
// or a max-sized low-S signature (e.g. 72 bytes) if use_max_sig is true // or a max-sized low-S signature (e.g. 72 bytes) if use_max_sig is true
bool CWallet::DummySignInput(CTxIn &tx_in, const CTxOut &txout, bool use_max_sig) const bool DummySignInput(const SigningProvider& provider, CTxIn &tx_in, const CTxOut &txout, bool use_max_sig)
{ {
// Fill in dummy signatures for fee calculation. // Fill in dummy signatures for fee calculation.
const CScript& scriptPubKey = txout.scriptPubKey; const CScript& scriptPubKey = txout.scriptPubKey;
SignatureData sigdata; SignatureData sigdata;
std::unique_ptr<SigningProvider> provider = GetSolvingProvider(scriptPubKey); if (!ProduceSignature(provider, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, scriptPubKey, sigdata)) {
if (!provider) {
// We don't know about this scriptpbuKey;
return false;
}
if (!ProduceSignature(*provider, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, scriptPubKey, sigdata)) {
return false; return false;
} }
UpdateInput(tx_in, sigdata); UpdateInput(tx_in, sigdata);
@ -1468,15 +1462,22 @@ bool CWallet::DummySignInput(CTxIn &tx_in, const CTxOut &txout, bool use_max_sig
} }
// Helper for producing a bunch of max-sized low-S low-R signatures (eg 71 bytes) // Helper for producing a bunch of max-sized low-S low-R signatures (eg 71 bytes)
bool CWallet::DummySignTx(CMutableTransaction &txNew, const std::vector<CTxOut> &txouts, bool use_max_sig) const bool CWallet::DummySignTx(CMutableTransaction &txNew, const std::vector<CTxOut> &txouts, const CCoinControl* coin_control) const
{ {
// Fill in dummy signatures for fee calculation. // Fill in dummy signatures for fee calculation.
int nIn = 0; int nIn = 0;
for (const auto& txout : txouts) for (const auto& txout : txouts)
{ {
if (!DummySignInput(txNew.vin[nIn], txout, use_max_sig)) { CTxIn& txin = txNew.vin[nIn];
// Use max sig if watch only inputs were used or if this particular input is an external input
// to ensure a sufficient fee is attained for the requested feerate.
const bool use_max_sig = coin_control && (coin_control->fAllowWatchOnly || coin_control->IsExternalSelected(txin.prevout));
const std::unique_ptr<SigningProvider> provider = GetSolvingProvider(txout.scriptPubKey);
if (!provider || !DummySignInput(*provider, txin, txout, use_max_sig)) {
if (!coin_control || !DummySignInput(coin_control->m_external_provider, txin, txout, use_max_sig)) {
return false; return false;
} }
}
nIn++; nIn++;
} }

View file

@ -576,14 +576,13 @@ public:
/** Pass this transaction to node for mempool insertion and relay to peers if flag set to true */ /** Pass this transaction to node for mempool insertion and relay to peers if flag set to true */
bool SubmitTxMemoryPoolAndRelay(const CWalletTx& wtx, std::string& err_string, bool relay) const; bool SubmitTxMemoryPoolAndRelay(const CWalletTx& wtx, std::string& err_string, bool relay) const;
bool DummySignTx(CMutableTransaction &txNew, const std::set<CTxOut> &txouts, bool use_max_sig = false) const bool DummySignTx(CMutableTransaction &txNew, const std::set<CTxOut> &txouts, const CCoinControl* coin_control = nullptr) const
{ {
std::vector<CTxOut> v_txouts(txouts.size()); std::vector<CTxOut> v_txouts(txouts.size());
std::copy(txouts.begin(), txouts.end(), v_txouts.begin()); std::copy(txouts.begin(), txouts.end(), v_txouts.begin());
return DummySignTx(txNew, v_txouts, use_max_sig); return DummySignTx(txNew, v_txouts, coin_control);
} }
bool DummySignTx(CMutableTransaction &txNew, const std::vector<CTxOut> &txouts, bool use_max_sig = false) const; bool DummySignTx(CMutableTransaction &txNew, const std::vector<CTxOut> &txouts, const CCoinControl* coin_control = nullptr) const;
bool DummySignInput(CTxIn &tx_in, const CTxOut &txout, bool use_max_sig = false) const;
bool ImportScripts(const std::set<CScript> scripts, int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool ImportScripts(const std::set<CScript> scripts, int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool ImportPrivKeys(const std::map<CKeyID, CKey>& privkey_map, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool ImportPrivKeys(const std::map<CKeyID, CKey>& privkey_map, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
@ -928,4 +927,6 @@ bool AddWalletSetting(interfaces::Chain& chain, const std::string& wallet_name);
//! Remove wallet name from persistent configuration so it will not be loaded on startup. //! Remove wallet name from persistent configuration so it will not be loaded on startup.
bool RemoveWalletSetting(interfaces::Chain& chain, const std::string& wallet_name); bool RemoveWalletSetting(interfaces::Chain& chain, const std::string& wallet_name);
bool DummySignInput(const SigningProvider& provider, CTxIn &tx_in, const CTxOut &txout, bool use_max_sig);
#endif // BITCOIN_WALLET_WALLET_H #endif // BITCOIN_WALLET_WALLET_H

View file

@ -8,6 +8,7 @@ from decimal import Decimal
from itertools import product from itertools import product
from test_framework.descriptors import descsum_create from test_framework.descriptors import descsum_create
from test_framework.key import ECKey
from test_framework.test_framework import BitcoinTestFramework from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import ( from test_framework.util import (
assert_approx, assert_approx,
@ -19,6 +20,7 @@ from test_framework.util import (
count_bytes, count_bytes,
find_vout_for_address, find_vout_for_address,
) )
from test_framework.wallet_util import bytes_to_wif
def get_unspent(listunspent, amount): def get_unspent(listunspent, amount):
@ -132,6 +134,7 @@ class RawTransactionsTest(BitcoinTestFramework):
self.test_subtract_fee_with_presets() self.test_subtract_fee_with_presets()
self.test_transaction_too_large() self.test_transaction_too_large()
self.test_include_unsafe() self.test_include_unsafe()
self.test_external_inputs()
self.test_22670() self.test_22670()
def test_change_position(self): def test_change_position(self):
@ -983,6 +986,56 @@ class RawTransactionsTest(BitcoinTestFramework):
wallet.sendmany("", outputs) wallet.sendmany("", outputs)
self.generate(self.nodes[0], 10) self.generate(self.nodes[0], 10)
assert_raises_rpc_error(-4, "Transaction too large", recipient.fundrawtransaction, rawtx) assert_raises_rpc_error(-4, "Transaction too large", recipient.fundrawtransaction, rawtx)
self.nodes[0].unloadwallet("large")
def test_external_inputs(self):
self.log.info("Test funding with external inputs")
eckey = ECKey()
eckey.generate()
privkey = bytes_to_wif(eckey.get_bytes())
self.nodes[2].createwallet("extfund")
wallet = self.nodes[2].get_wallet_rpc("extfund")
# Make a weird but signable script. sh(pkh()) descriptor accomplishes this
desc = descsum_create("sh(pkh({}))".format(privkey))
if self.options.descriptors:
res = self.nodes[0].importdescriptors([{"desc": desc, "timestamp": "now"}])
else:
res = self.nodes[0].importmulti([{"desc": desc, "timestamp": "now"}])
assert res[0]["success"]
addr = self.nodes[0].deriveaddresses(desc)[0]
addr_info = self.nodes[0].getaddressinfo(addr)
self.nodes[0].sendtoaddress(addr, 10)
self.nodes[0].sendtoaddress(wallet.getnewaddress(), 10)
self.nodes[0].generate(6)
ext_utxo = self.nodes[0].listunspent(addresses=[addr])[0]
# An external input without solving data should result in an error
raw_tx = wallet.createrawtransaction([ext_utxo], {self.nodes[0].getnewaddress(): 15})
assert_raises_rpc_error(-4, "Insufficient funds", wallet.fundrawtransaction, raw_tx)
# Error conditions
assert_raises_rpc_error(-5, "'not a pubkey' is not hex", wallet.fundrawtransaction, raw_tx, {"solving_data": {"pubkeys":["not a pubkey"]}})
assert_raises_rpc_error(-5, "'01234567890a0b0c0d0e0f' is not a valid public key", wallet.fundrawtransaction, raw_tx, {"solving_data": {"pubkeys":["01234567890a0b0c0d0e0f"]}})
assert_raises_rpc_error(-5, "'not a script' is not hex", wallet.fundrawtransaction, raw_tx, {"solving_data": {"scripts":["not a script"]}})
assert_raises_rpc_error(-8, "Unable to parse descriptor 'not a descriptor'", wallet.fundrawtransaction, raw_tx, {"solving_data": {"descriptors":["not a descriptor"]}})
# But funding should work when the solving data is provided
funded_tx = wallet.fundrawtransaction(raw_tx, {"solving_data": {"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]}})
signed_tx = wallet.signrawtransactionwithwallet(funded_tx['hex'])
assert not signed_tx['complete']
signed_tx = self.nodes[0].signrawtransactionwithwallet(signed_tx['hex'])
assert signed_tx['complete']
funded_tx = wallet.fundrawtransaction(raw_tx, {"solving_data": {"descriptors": [desc]}})
signed_tx = wallet.signrawtransactionwithwallet(funded_tx['hex'])
assert not signed_tx['complete']
signed_tx = self.nodes[0].signrawtransactionwithwallet(signed_tx['hex'])
assert signed_tx['complete']
self.nodes[2].unloadwallet("extfund")
def test_include_unsafe(self): def test_include_unsafe(self):
self.log.info("Test fundrawtxn with unsafe inputs") self.log.info("Test fundrawtxn with unsafe inputs")
@ -1017,6 +1070,7 @@ class RawTransactionsTest(BitcoinTestFramework):
assert all((txin["txid"], txin["vout"]) in inputs for txin in tx_dec["vin"]) assert all((txin["txid"], txin["vout"]) in inputs for txin in tx_dec["vin"])
signedtx = wallet.signrawtransactionwithwallet(fundedtx['hex']) signedtx = wallet.signrawtransactionwithwallet(fundedtx['hex'])
assert wallet.testmempoolaccept([signedtx['hex']])[0]["allowed"] assert wallet.testmempoolaccept([signedtx['hex']])[0]["allowed"]
self.nodes[0].unloadwallet("unsafe")
def test_22670(self): def test_22670(self):
# In issue #22670, it was observed that ApproximateBestSubset may # In issue #22670, it was observed that ApproximateBestSubset may

View file

@ -8,6 +8,8 @@
from decimal import Decimal from decimal import Decimal
from itertools import product from itertools import product
from test_framework.descriptors import descsum_create
from test_framework.key import ECKey
from test_framework.test_framework import BitcoinTestFramework from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import ( from test_framework.util import (
assert_approx, assert_approx,
@ -16,6 +18,7 @@ from test_framework.util import (
assert_raises_rpc_error, assert_raises_rpc_error,
find_output, find_output,
) )
from test_framework.wallet_util import bytes_to_wif
import json import json
import os import os
@ -608,5 +611,42 @@ class PSBTTest(BitcoinTestFramework):
assert_raises_rpc_error(-25, 'Inputs missing or spent', self.nodes[0].walletprocesspsbt, 'cHNidP8BAJoCAAAAAkvEW8NnDtdNtDpsmze+Ht2LH35IJcKv00jKAlUs21RrAwAAAAD/////S8Rbw2cO1020OmybN74e3Ysffkglwq/TSMoCVSzbVGsBAAAAAP7///8CwLYClQAAAAAWABSNJKzjaUb3uOxixsvh1GGE3fW7zQD5ApUAAAAAFgAUKNw0x8HRctAgmvoevm4u1SbN7XIAAAAAAAEAnQIAAAACczMa321tVHuN4GKWKRncycI22aX3uXgwSFUKM2orjRsBAAAAAP7///9zMxrfbW1Ue43gYpYpGdzJwjbZpfe5eDBIVQozaiuNGwAAAAAA/v///wIA+QKVAAAAABl2qRT9zXUVA8Ls5iVqynLHe5/vSe1XyYisQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAAAAAQEfQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAA==') assert_raises_rpc_error(-25, 'Inputs missing or spent', self.nodes[0].walletprocesspsbt, 'cHNidP8BAJoCAAAAAkvEW8NnDtdNtDpsmze+Ht2LH35IJcKv00jKAlUs21RrAwAAAAD/////S8Rbw2cO1020OmybN74e3Ysffkglwq/TSMoCVSzbVGsBAAAAAP7///8CwLYClQAAAAAWABSNJKzjaUb3uOxixsvh1GGE3fW7zQD5ApUAAAAAFgAUKNw0x8HRctAgmvoevm4u1SbN7XIAAAAAAAEAnQIAAAACczMa321tVHuN4GKWKRncycI22aX3uXgwSFUKM2orjRsBAAAAAP7///9zMxrfbW1Ue43gYpYpGdzJwjbZpfe5eDBIVQozaiuNGwAAAAAA/v///wIA+QKVAAAAABl2qRT9zXUVA8Ls5iVqynLHe5/vSe1XyYisQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAAAAAQEfQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAA==')
# Test that we can fund psbts with external inputs specified
eckey = ECKey()
eckey.generate()
privkey = bytes_to_wif(eckey.get_bytes())
# Make a weird but signable script. sh(pkh()) descriptor accomplishes this
desc = descsum_create("sh(pkh({}))".format(privkey))
if self.options.descriptors:
res = self.nodes[0].importdescriptors([{"desc": desc, "timestamp": "now"}])
else:
res = self.nodes[0].importmulti([{"desc": desc, "timestamp": "now"}])
assert res[0]["success"]
addr = self.nodes[0].deriveaddresses(desc)[0]
addr_info = self.nodes[0].getaddressinfo(addr)
self.nodes[0].sendtoaddress(addr, 10)
self.nodes[0].generate(6)
ext_utxo = self.nodes[0].listunspent(addresses=[addr])[0]
# An external input without solving data should result in an error
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[1].walletcreatefundedpsbt, [ext_utxo], {self.nodes[0].getnewaddress(): 10 + ext_utxo['amount']}, 0, {'add_inputs': True})
# But funding should work when the solving data is provided
psbt = self.nodes[1].walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {'add_inputs': True, "solving_data": {"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]}})
signed = self.nodes[1].walletprocesspsbt(psbt['psbt'])
assert not signed['complete']
signed = self.nodes[0].walletprocesspsbt(signed['psbt'])
assert signed['complete']
self.nodes[0].finalizepsbt(signed['psbt'])
psbt = self.nodes[1].walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {'add_inputs': True, "solving_data":{"descriptors": [desc]}})
signed = self.nodes[1].walletprocesspsbt(psbt['psbt'])
assert not signed['complete']
signed = self.nodes[0].walletprocesspsbt(signed['psbt'])
assert signed['complete']
self.nodes[0].finalizepsbt(signed['psbt'])
if __name__ == '__main__': if __name__ == '__main__':
PSBTTest().main() PSBTTest().main()

View file

@ -9,6 +9,7 @@ from itertools import product
from test_framework.authproxy import JSONRPCException from test_framework.authproxy import JSONRPCException
from test_framework.descriptors import descsum_create from test_framework.descriptors import descsum_create
from test_framework.key import ECKey
from test_framework.test_framework import BitcoinTestFramework from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import ( from test_framework.util import (
assert_equal, assert_equal,
@ -16,6 +17,7 @@ from test_framework.util import (
assert_greater_than, assert_greater_than,
assert_raises_rpc_error, assert_raises_rpc_error,
) )
from test_framework.wallet_util import bytes_to_wif
class WalletSendTest(BitcoinTestFramework): class WalletSendTest(BitcoinTestFramework):
def set_test_params(self): def set_test_params(self):
@ -35,7 +37,7 @@ class WalletSendTest(BitcoinTestFramework):
conf_target=None, estimate_mode=None, fee_rate=None, add_to_wallet=None, psbt=None, conf_target=None, estimate_mode=None, fee_rate=None, add_to_wallet=None, psbt=None,
inputs=None, add_inputs=None, include_unsafe=None, change_address=None, change_position=None, change_type=None, inputs=None, add_inputs=None, include_unsafe=None, change_address=None, change_position=None, change_type=None,
include_watching=None, locktime=None, lock_unspents=None, replaceable=None, subtract_fee_from_outputs=None, include_watching=None, locktime=None, lock_unspents=None, replaceable=None, subtract_fee_from_outputs=None,
expect_error=None): expect_error=None, solving_data=None):
assert (amount is None) != (data is None) assert (amount is None) != (data is None)
from_balance_before = from_wallet.getbalances()["mine"]["trusted"] from_balance_before = from_wallet.getbalances()["mine"]["trusted"]
@ -94,6 +96,8 @@ class WalletSendTest(BitcoinTestFramework):
options["replaceable"] = replaceable options["replaceable"] = replaceable
if subtract_fee_from_outputs is not None: if subtract_fee_from_outputs is not None:
options["subtract_fee_from_outputs"] = subtract_fee_from_outputs options["subtract_fee_from_outputs"] = subtract_fee_from_outputs
if solving_data is not None:
options["solving_data"] = solving_data
if len(options.keys()) == 0: if len(options.keys()) == 0:
options = None options = None
@ -476,6 +480,46 @@ class WalletSendTest(BitcoinTestFramework):
res = self.test_send(from_wallet=w5, to_wallet=w0, amount=1, include_unsafe=True) res = self.test_send(from_wallet=w5, to_wallet=w0, amount=1, include_unsafe=True)
assert res["complete"] assert res["complete"]
self.log.info("External outputs")
eckey = ECKey()
eckey.generate()
privkey = bytes_to_wif(eckey.get_bytes())
self.nodes[1].createwallet("extsend")
ext_wallet = self.nodes[1].get_wallet_rpc("extsend")
self.nodes[1].createwallet("extfund")
ext_fund = self.nodes[1].get_wallet_rpc("extfund")
# Make a weird but signable script. sh(pkh()) descriptor accomplishes this
desc = descsum_create("sh(pkh({}))".format(privkey))
if self.options.descriptors:
res = ext_fund.importdescriptors([{"desc": desc, "timestamp": "now"}])
else:
res = ext_fund.importmulti([{"desc": desc, "timestamp": "now"}])
assert res[0]["success"]
addr = self.nodes[0].deriveaddresses(desc)[0]
addr_info = ext_fund.getaddressinfo(addr)
self.nodes[0].sendtoaddress(addr, 10)
self.nodes[0].sendtoaddress(ext_wallet.getnewaddress(), 10)
self.nodes[0].generate(6)
ext_utxo = ext_fund.listunspent(addresses=[addr])[0]
# An external input without solving data should result in an error
self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, expect_error=(-4, "Insufficient funds"))
# But funding should work when the solving data is provided
res = self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, solving_data={"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]})
signed = ext_wallet.walletprocesspsbt(res["psbt"])
signed = ext_fund.walletprocesspsbt(res["psbt"])
assert signed["complete"]
self.nodes[0].finalizepsbt(signed["psbt"])
res = self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, solving_data={"descriptors": [desc]})
signed = ext_wallet.walletprocesspsbt(res["psbt"])
signed = ext_fund.walletprocesspsbt(res["psbt"])
assert signed["complete"]
self.nodes[0].finalizepsbt(signed["psbt"])
if __name__ == '__main__': if __name__ == '__main__':
WalletSendTest().main() WalletSendTest().main()