Skip to content

Commit 573b462

Browse files
committed
Merge bitcoin/bitcoin#17211: Allow fundrawtransaction and walletcreatefundedpsbt to take external inputs
928af61 allow send rpc take external inputs and solving data (Andrew Chow) e39b5a5 Tests for funding with external inputs (Andrew Chow) 38f5642 allow fundtx rpcs to work with external inputs (Andrew Chow) d5cfb86 Allow Coin Selection be able to take external inputs (Andrew Chow) a00eb38 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 bitcoin/bitcoin@928af61 meshcollider: Re-utACK 928af61 instagibbs: crACK 928af61 yanmaani: utACK 928af61. Tree-SHA512: bc7a6ef8961a7f4971ea5985d75e2d6dc50c2a90b44c664a1c4b0f1be5c1c97823516358fdaab35771a4701dbefc0862127b1d0d4bfd02b4f20d2befa4434700
2 parents 446b706 + 928af61 commit 573b462

File tree

10 files changed

+381
-50
lines changed

10 files changed

+381
-50
lines changed

src/wallet/coincontrol.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99
#include <policy/feerate.h>
1010
#include <policy/fees.h>
1111
#include <primitives/transaction.h>
12+
#include <script/keyorigin.h>
13+
#include <script/signingprovider.h>
1214
#include <script/standard.h>
1315

1416
#include <optional>
17+
#include <algorithm>
18+
#include <map>
19+
#include <set>
1520

1621
const int DEFAULT_MIN_DEPTH = 0;
1722
const int DEFAULT_MAX_DEPTH = 9999999;
@@ -53,6 +58,8 @@ class CCoinControl
5358
int m_min_depth = DEFAULT_MIN_DEPTH;
5459
//! Maximum chain depth value for coin availability
5560
int m_max_depth = DEFAULT_MAX_DEPTH;
61+
//! SigningProvider that has pubkeys and scripts to do spend size estimation for external inputs
62+
FlatSigningProvider m_external_provider;
5663

5764
CCoinControl();
5865

@@ -66,11 +73,32 @@ class CCoinControl
6673
return (setSelected.count(output) > 0);
6774
}
6875

76+
bool IsExternalSelected(const COutPoint& output) const
77+
{
78+
return (m_external_txouts.count(output) > 0);
79+
}
80+
81+
bool GetExternalOutput(const COutPoint& outpoint, CTxOut& txout) const
82+
{
83+
const auto ext_it = m_external_txouts.find(outpoint);
84+
if (ext_it == m_external_txouts.end()) {
85+
return false;
86+
}
87+
txout = ext_it->second;
88+
return true;
89+
}
90+
6991
void Select(const COutPoint& output)
7092
{
7193
setSelected.insert(output);
7294
}
7395

96+
void Select(const COutPoint& outpoint, const CTxOut& txout)
97+
{
98+
setSelected.insert(outpoint);
99+
m_external_txouts.emplace(outpoint, txout);
100+
}
101+
74102
void UnSelect(const COutPoint& output)
75103
{
76104
setSelected.erase(output);
@@ -88,6 +116,7 @@ class CCoinControl
88116

89117
private:
90118
std::set<COutPoint> setSelected;
119+
std::map<COutPoint, CTxOut> m_external_txouts;
91120
};
92121

93122
#endif // BITCOIN_WALLET_COINCONTROL_H

src/wallet/coinselection.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ class CInputCoin {
3737
m_input_bytes = input_bytes;
3838
}
3939

40+
CInputCoin(const COutPoint& outpoint_in, const CTxOut& txout_in)
41+
{
42+
outpoint = outpoint_in;
43+
txout = txout_in;
44+
effective_value = txout.nValue;
45+
}
46+
47+
CInputCoin(const COutPoint& outpoint_in, const CTxOut& txout_in, int input_bytes) : CInputCoin(outpoint_in, txout_in)
48+
{
49+
m_input_bytes = input_bytes;
50+
}
51+
4052
COutPoint outpoint;
4153
CTxOut txout;
4254
CAmount effective_value;

src/wallet/rpcwallet.cpp

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343

4444
#include <univalue.h>
4545

46+
#include <map>
4647

4748
using interfaces::FoundBlock;
4849

@@ -3213,6 +3214,7 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out,
32133214
{"fee_rate", UniValueType()}, // will be checked by AmountFromValue() in SetFeeEstimateMode()
32143215
{"feeRate", UniValueType()}, // will be checked by AmountFromValue() below
32153216
{"psbt", UniValueType(UniValue::VBOOL)},
3217+
{"solving_data", UniValueType(UniValue::VOBJ)},
32163218
{"subtractFeeFromOutputs", UniValueType(UniValue::VARR)},
32173219
{"subtract_fee_from_outputs", UniValueType(UniValue::VARR)},
32183220
{"replaceable", UniValueType(UniValue::VBOOL)},
@@ -3289,6 +3291,54 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out,
32893291
coinControl.fAllowWatchOnly = ParseIncludeWatchonly(NullUniValue, wallet);
32903292
}
32913293

3294+
if (options.exists("solving_data")) {
3295+
UniValue solving_data = options["solving_data"].get_obj();
3296+
if (solving_data.exists("pubkeys")) {
3297+
for (const UniValue& pk_univ : solving_data["pubkeys"].get_array().getValues()) {
3298+
const std::string& pk_str = pk_univ.get_str();
3299+
if (!IsHex(pk_str)) {
3300+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("'%s' is not hex", pk_str));
3301+
}
3302+
const std::vector<unsigned char> data(ParseHex(pk_str));
3303+
CPubKey pubkey(data.begin(), data.end());
3304+
if (!pubkey.IsFullyValid()) {
3305+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("'%s' is not a valid public key", pk_str));
3306+
}
3307+
coinControl.m_external_provider.pubkeys.emplace(pubkey.GetID(), pubkey);
3308+
// Add witness script for pubkeys
3309+
const CScript wit_script = GetScriptForDestination(WitnessV0KeyHash(pubkey));
3310+
coinControl.m_external_provider.scripts.emplace(CScriptID(wit_script), wit_script);
3311+
}
3312+
}
3313+
3314+
if (solving_data.exists("scripts")) {
3315+
for (const UniValue& script_univ : solving_data["scripts"].get_array().getValues()) {
3316+
const std::string& script_str = script_univ.get_str();
3317+
if (!IsHex(script_str)) {
3318+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("'%s' is not hex", script_str));
3319+
}
3320+
std::vector<unsigned char> script_data(ParseHex(script_str));
3321+
const CScript script(script_data.begin(), script_data.end());
3322+
coinControl.m_external_provider.scripts.emplace(CScriptID(script), script);
3323+
}
3324+
}
3325+
3326+
if (solving_data.exists("descriptors")) {
3327+
for (const UniValue& desc_univ : solving_data["descriptors"].get_array().getValues()) {
3328+
const std::string& desc_str = desc_univ.get_str();
3329+
FlatSigningProvider desc_out;
3330+
std::string error;
3331+
std::vector<CScript> scripts_temp;
3332+
std::unique_ptr<Descriptor> desc = Parse(desc_str, desc_out, error, true);
3333+
if (!desc) {
3334+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Unable to parse descriptor '%s': %s", desc_str, error));
3335+
}
3336+
desc->Expand(0, desc_out, scripts_temp, desc_out);
3337+
coinControl.m_external_provider = Merge(coinControl.m_external_provider, desc_out);
3338+
}
3339+
}
3340+
}
3341+
32923342
if (tx.vout.size() == 0)
32933343
throw JSONRPCError(RPC_INVALID_PARAMETER, "TX must have at least one output");
32943344

@@ -3306,6 +3356,19 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out,
33063356
setSubtractFeeFromOutputs.insert(pos);
33073357
}
33083358

3359+
// Fetch specified UTXOs from the UTXO set to get the scriptPubKeys and values of the outputs being selected
3360+
// and to match with the given solving_data. Only used for non-wallet outputs.
3361+
std::map<COutPoint, Coin> coins;
3362+
for (const CTxIn& txin : tx.vin) {
3363+
coins[txin.prevout]; // Create empty map entry keyed by prevout.
3364+
}
3365+
wallet.chain().findCoins(coins);
3366+
for (const auto& coin : coins) {
3367+
if (!coin.second.out.IsNull()) {
3368+
coinControl.Select(coin.first, coin.second.out);
3369+
}
3370+
}
3371+
33093372
bilingual_str error;
33103373

33113374
if (!FundTransaction(wallet, tx, fee_out, change_position, error, lockUnspents, setSubtractFeeFromOutputs, coinControl)) {
@@ -3321,8 +3384,9 @@ static RPCHelpMan fundrawtransaction()
33213384
"No existing outputs will be modified unless \"subtractFeeFromOutputs\" is specified.\n"
33223385
"Note that inputs which were signed may need to be resigned after completion since in/outputs have been added.\n"
33233386
"The inputs added will not be signed, use signrawtransactionwithkey\n"
3324-
" or signrawtransactionwithwallet for that.\n"
3325-
"Note that all existing inputs must have their previous output transaction be in the wallet.\n"
3387+
"or signrawtransactionwithwallet for that.\n"
3388+
"All existing inputs must either have their previous output transaction be in the wallet\n"
3389+
"or be in the UTXO set. Solving data must be provided for non-wallet inputs.\n"
33263390
"Note that all inputs selected must be of standard form and P2SH scripts must be\n"
33273391
"in the wallet using importaddress or addmultisigaddress (to calculate fees).\n"
33283392
"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()
33573421
{"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks"},
33583422
{"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"unset"}, std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
33593423
" \"" + FeeModes("\"\n\"") + "\""},
3424+
{"solving_data", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "Keys and scripts needed for producing a final transaction with a dummy signature.\n"
3425+
"Used for fee estimation during coin selection.",
3426+
{
3427+
{"pubkeys", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Public keys involved in this transaction.",
3428+
{
3429+
{"pubkey", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A public key"},
3430+
},
3431+
},
3432+
{"scripts", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Scripts involved in this transaction.",
3433+
{
3434+
{"script", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A script"},
3435+
},
3436+
},
3437+
{"descriptors", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Descriptors that provide solving data for this transaction.",
3438+
{
3439+
{"descriptor", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A descriptor"},
3440+
},
3441+
}
3442+
}
3443+
},
33603444
},
33613445
"options"},
33623446
{"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()
42024286
},
42034287
{"replaceable", RPCArg::Type::BOOL, RPCArg::DefaultHint{"wallet default"}, "Marks this transaction as BIP125 replaceable.\n"
42044288
"Allows this transaction to be replaced by a transaction with higher fees"},
4289+
{"solving_data", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "Keys and scripts needed for producing a final transaction with a dummy signature.\n"
4290+
"Used for fee estimation during coin selection.",
4291+
{
4292+
{"pubkeys", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Public keys involved in this transaction.",
4293+
{
4294+
{"pubkey", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A public key"},
4295+
},
4296+
},
4297+
{"scripts", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Scripts involved in this transaction.",
4298+
{
4299+
{"script", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A script"},
4300+
},
4301+
},
4302+
{"descriptors", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Descriptors that provide solving data for this transaction.",
4303+
{
4304+
{"descriptor", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A descriptor"},
4305+
},
4306+
}
4307+
}
4308+
},
42054309
},
42064310
"options"},
42074311
},
@@ -4489,7 +4593,9 @@ static RPCHelpMan walletcreatefundedpsbt()
44894593
{
44904594
return RPCHelpMan{"walletcreatefundedpsbt",
44914595
"\nCreates and funds a transaction in the Partially Signed Transaction format.\n"
4492-
"Implements the Creator and Updater roles.\n",
4596+
"Implements the Creator and Updater roles.\n"
4597+
"All existing inputs must either have their previous output transaction be in the wallet\n"
4598+
"or be in the UTXO set. Solving data must be provided for non-wallet inputs.\n",
44934599
{
44944600
{"inputs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, "Leave empty to add inputs automatically. See add_inputs option.",
44954601
{
@@ -4546,6 +4652,26 @@ static RPCHelpMan walletcreatefundedpsbt()
45464652
{"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks"},
45474653
{"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"unset"}, std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
45484654
" \"" + FeeModes("\"\n\"") + "\""},
4655+
{"solving_data", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "Keys and scripts needed for producing a final transaction with a dummy signature.\n"
4656+
"Used for fee estimation during coin selection.",
4657+
{
4658+
{"pubkeys", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Public keys involved in this transaction.",
4659+
{
4660+
{"pubkey", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A public key"},
4661+
},
4662+
},
4663+
{"scripts", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Scripts involved in this transaction.",
4664+
{
4665+
{"script", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A script"},
4666+
},
4667+
},
4668+
{"descriptors", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Descriptors that provide solving data for this transaction.",
4669+
{
4670+
{"descriptor", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A descriptor"},
4671+
},
4672+
}
4673+
}
4674+
},
45494675
},
45504676
"options"},
45514677
{"bip32derivs", RPCArg::Type::BOOL, RPCArg::Default{true}, "Include BIP 32 derivation paths for public keys if we know them"},

0 commit comments

Comments
 (0)