Skip to content

Commit 526e802

Browse files
committed
Merge #755: Be able to fund transactions with peg-ins specified
00eb6c0 Test funding peg-in psbt (Andrew Chow) 1ce74e0 Be able to fund transactions with peg-ins (Andrew Chow) 74c4d37 Tests for fundrawtransaction with external inputs (Andrew Chow) c258746 allow fundtx rpcs to work with external inputs (Andrew Chow) 9486cdf Give a better error when tx size estimation fails (Andrew Chow) 8258aaa Allow Coin Selection be able to take external inputs (Andrew Chow) 698340b Allow CInputCoin to also be constructed with COutPoint and CTxOut (Andrew Chow) Pull request description: Allows `fundrawtransaction` and `walletcreatefundedpsbt` to take transactions with pre-selected peg-in inputs and take them into account when selecting additional inputs to meet the output amounts. As a side effect, those RPCs will also now allow pre-selected non-wallet inputs so long as the correct additional solving data is provided (scripts and pubkeys are needed to estimate the size for transaction fees). Based on #751 Ports bitcoin/bitcoin#17211 with some modification to work with elements outputs and such. Tree-SHA512: 813202adfe2b63780bea01b02975dda393197a489a6c1a9590908ac7252d1d0e5f0984125fbfd1dc271524ddad84dc1778a1e8fa447e61235c7f21c6e6999460
2 parents 2949ea9 + 00eb6c0 commit 526e802

File tree

10 files changed

+320
-44
lines changed

10 files changed

+320
-44
lines changed

src/rpc/client.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,13 @@ static const CRPCConvertParam vRPCConvertParams[] =
100100
{ "combinerawtransaction", 0, "txs" },
101101
{ "fundrawtransaction", 1, "options" },
102102
{ "fundrawtransaction", 2, "iswitness" },
103+
{ "fundrawtransaction", 3, "solving_data" },
103104
{ "walletcreatefundedpsbt", 0, "inputs" },
104105
{ "walletcreatefundedpsbt", 1, "outputs" },
105106
{ "walletcreatefundedpsbt", 2, "locktime" },
106107
{ "walletcreatefundedpsbt", 3, "options" },
107108
{ "walletcreatefundedpsbt", 4, "bip32derivs" },
109+
{ "walletcreatefundedpsbt", 5, "solving_data" },
108110
{ "walletprocesspsbt", 1, "sign" },
109111
{ "walletprocesspsbt", 3, "bip32derivs" },
110112
{ "walletfillpsbtdata", 1, "bip32derivs" },

src/wallet/coincontrol.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,7 @@ void CCoinControl::SetNull()
1919
m_confirm_target.reset();
2020
m_signal_bip125_rbf.reset();
2121
m_fee_mode = FeeEstimateMode::UNSET;
22+
m_external_txouts.clear();
23+
m_external_provider = FlatSigningProvider();
2224
}
2325

src/wallet/coincontrol.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ class CCoinControl
3737
bool m_avoid_partial_spends;
3838
//! Fee estimation mode to control arguments to estimateSmartFee
3939
FeeEstimateMode m_fee_mode;
40+
//! SigningProvider that has pubkeys and scripts to do spend size estimation for external inputs
41+
FlatSigningProvider m_external_provider;
4042

4143
CCoinControl()
4244
{
@@ -55,11 +57,42 @@ class CCoinControl
5557
return (setSelected.count(output) > 0);
5658
}
5759

60+
bool IsExternalSelected(const COutPoint& output) const
61+
{
62+
return (m_external_txouts.count(output) > 0);
63+
}
64+
65+
bool GetExternalOutput(const COutPoint& outpoint, CTxOut& txout) const
66+
{
67+
const auto ext_it = m_external_txouts.find(outpoint);
68+
if (ext_it == m_external_txouts.end()) {
69+
return false;
70+
}
71+
txout = ext_it->second;
72+
return true;
73+
}
74+
5875
void Select(const COutPoint& output)
5976
{
6077
setSelected.insert(output);
6178
}
6279

80+
void SelectExternal(const COutPoint& outpoint, const CTxOut& txout)
81+
{
82+
setSelected.insert(outpoint);
83+
m_external_txouts.emplace(outpoint, txout);
84+
}
85+
86+
void Select(const COutPoint& outpoint, const Sidechain::Bitcoin::CTxOut& txout_in)
87+
{
88+
setSelected.insert(outpoint);
89+
CTxOut txout;
90+
txout.scriptPubKey = txout_in.scriptPubKey;
91+
txout.nValue.SetToAmount(txout_in.nValue);
92+
txout.nAsset.SetToAsset(Params().GetConsensus().pegged_asset);
93+
m_external_txouts.emplace(outpoint, txout);
94+
}
95+
6396
void UnSelect(const COutPoint& output)
6497
{
6598
setSelected.erase(output);
@@ -77,6 +110,7 @@ class CCoinControl
77110

78111
private:
79112
std::set<COutPoint> setSelected;
113+
std::map<COutPoint, CTxOut> m_external_txouts;
80114
};
81115

82116
#endif // BITCOIN_WALLET_COINCONTROL_H

src/wallet/coinselection.h

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
#define BITCOIN_WALLET_COINSELECTION_H
77

88
#include <amount.h>
9+
#include <chainparams.h>
910
#include <primitives/transaction.h>
11+
#include <primitives/bitcoin/transaction.h>
1012
#include <random.h>
1113

1214
//! target minimum change amount
@@ -26,6 +28,41 @@ class CInputCoin {
2628
m_input_bytes = input_bytes;
2729
}
2830

31+
CInputCoin(const COutPoint& outpoint_in, const CTxOut& txout_in)
32+
{
33+
outpoint = outpoint_in;
34+
txout = txout_in;
35+
if (txout.nValue.IsExplicit()) {
36+
effective_value = txout_in.nValue.GetAmount();
37+
value = txout.nValue.GetAmount();
38+
asset = txout.nAsset.GetAsset();
39+
} else {
40+
effective_value = 0;
41+
}
42+
}
43+
44+
CInputCoin(const COutPoint& outpoint_in, const CTxOut& txout_in, int input_bytes) : CInputCoin(outpoint_in, txout_in)
45+
{
46+
m_input_bytes = input_bytes;
47+
}
48+
49+
CInputCoin(const COutPoint& outpoint_in, const Sidechain::Bitcoin::CTxOut& txout_in)
50+
{
51+
outpoint = outpoint_in;
52+
effective_value = txout_in.nValue;
53+
txout.SetNull();
54+
txout.scriptPubKey = txout_in.scriptPubKey;
55+
txout.nValue.SetToAmount(txout_in.nValue);
56+
txout.nAsset.SetToAsset(Params().GetConsensus().pegged_asset);
57+
asset = Params().GetConsensus().pegged_asset;
58+
value = txout_in.nValue;
59+
}
60+
61+
CInputCoin(const COutPoint& outpoint_in, const Sidechain::Bitcoin::CTxOut& txout_in, int input_bytes) : CInputCoin(outpoint_in, txout_in)
62+
{
63+
m_input_bytes = input_bytes;
64+
}
65+
2966
COutPoint outpoint;
3067
CTxOut txout;
3168
CAmount effective_value;

src/wallet/rpcwallet.cpp

Lines changed: 155 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3212,7 +3212,7 @@ static UniValue listunspent(const JSONRPCRequest& request)
32123212
return results;
32133213
}
32143214

3215-
void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& fee_out, int& change_position, UniValue options)
3215+
void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& fee_out, int& change_position, UniValue options, const UniValue& solving_data)
32163216
{
32173217
// Make sure the results are valid at least up to the most recent block
32183218
// the user could have gotten from another RPC command prior to now
@@ -3330,6 +3330,40 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
33303330
}
33313331
}
33323332

3333+
if (!solving_data.isNull()) {
3334+
if (solving_data.exists("pubkeys")) {
3335+
UniValue pubkey_strs = solving_data["pubkeys"].get_array();
3336+
for (unsigned int i = 0; i < pubkey_strs.size(); ++i) {
3337+
std::vector<unsigned char> data(ParseHex(pubkey_strs[i].get_str()));
3338+
CPubKey pubkey(data.begin(), data.end());
3339+
if (!pubkey.IsFullyValid()) {
3340+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("%s is not a valid public key", pubkey_strs[i].get_str()));
3341+
}
3342+
coinControl.m_external_provider.pubkeys.emplace(pubkey.GetID(), pubkey);
3343+
// Add witnes script for pubkeys
3344+
CScript wit_script = GetScriptForDestination(WitnessV0KeyHash(pubkey.GetID()));
3345+
coinControl.m_external_provider.scripts.emplace(CScriptID(wit_script), wit_script);
3346+
}
3347+
}
3348+
3349+
if (solving_data.exists("scripts")) {
3350+
UniValue script_strs = solving_data["scripts"].get_array();
3351+
for (unsigned int i = 0; i < script_strs.size(); ++i) {
3352+
CScript script = ParseScript(script_strs[i].get_str());
3353+
coinControl.m_external_provider.scripts.emplace(CScriptID(script), script);
3354+
}
3355+
}
3356+
3357+
if (solving_data.exists("descriptors")) {
3358+
UniValue desc_strs = solving_data["descriptors"].get_array();
3359+
for (unsigned int i = 0; i < desc_strs.size(); ++i) {
3360+
FlatSigningProvider desc_out;
3361+
std::unique_ptr<Descriptor> desc = Parse(desc_strs[i].get_str(), desc_out, true);
3362+
coinControl.m_external_provider = Merge(coinControl.m_external_provider, desc_out);
3363+
}
3364+
}
3365+
}
3366+
33333367
if (tx.vout.size() == 0)
33343368
throw JSONRPCError(RPC_INVALID_PARAMETER, "TX must have at least one output");
33353369

@@ -3347,6 +3381,42 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
33473381
setSubtractFeeFromOutputs.insert(pos);
33483382
}
33493383

3384+
// Check any existing inputs for peg-in data and add to external txouts if so
3385+
// Fetch specified UTXOs from the UTXO set
3386+
const auto& fedpegscripts = GetValidFedpegScripts(chainActive.Tip(), Params().GetConsensus(), true /* nextblock_validation */);
3387+
std::map<COutPoint, Coin> coins;
3388+
for (unsigned int i = 0; i < tx.vin.size(); ++i ) {
3389+
const CTxIn& txin = tx.vin[i];
3390+
coins[txin.prevout]; // Create empty map entry keyed by prevout.
3391+
if (txin.m_is_pegin) {
3392+
std::string err;
3393+
if (tx.witness.vtxinwit.size() != tx.vin.size() || !IsValidPeginWitness(tx.witness.vtxinwit[i].m_pegin_witness, fedpegscripts, txin.prevout, err, false)) {
3394+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Transaction contains invalid peg-in input: %s", err));
3395+
}
3396+
CScriptWitness& pegin_witness = tx.witness.vtxinwit[i].m_pegin_witness;
3397+
CTxOut txout = GetPeginOutputFromWitness(pegin_witness);
3398+
coinControl.SelectExternal(txin.prevout, txout);
3399+
}
3400+
}
3401+
CCoinsView viewDummy;
3402+
CCoinsViewCache view(&viewDummy);
3403+
{
3404+
LOCK2(cs_main, mempool.cs);
3405+
CCoinsViewCache& chain_view = *pcoinsTip;
3406+
CCoinsViewMemPool mempool_view(&chain_view, mempool);
3407+
for (auto& coin : coins) {
3408+
if (!mempool_view.GetCoin(coin.first, coin.second)) {
3409+
// Either the coin is not in the CCoinsViewCache or is spent. Clear it.
3410+
coin.second.Clear();
3411+
}
3412+
}
3413+
}
3414+
for (const auto& coin : coins) {
3415+
if (!coin.second.out.IsNull()) {
3416+
coinControl.SelectExternal(coin.first, coin.second.out);
3417+
}
3418+
}
3419+
33503420
std::string strFailReason;
33513421

33523422
if (!pwallet->FundTransaction(tx, fee_out, change_position, strFailReason, lockUnspents, setSubtractFeeFromOutputs, coinControl)) {
@@ -3363,7 +3433,7 @@ static UniValue fundrawtransaction(const JSONRPCRequest& request)
33633433
return NullUniValue;
33643434
}
33653435

3366-
if (request.fHelp || request.params.size() < 1 || request.params.size() > 3)
3436+
if (request.fHelp || request.params.size() < 1 || request.params.size() > 4)
33673437
throw std::runtime_error(
33683438
RPCHelpMan{"fundrawtransaction",
33693439
"\nAdd inputs to a transaction until it has enough in value to meet its out value.\n"
@@ -3406,6 +3476,25 @@ static UniValue fundrawtransaction(const JSONRPCRequest& request)
34063476
"options"},
34073477
{"iswitness", RPCArg::Type::BOOL, /* default */ "depends on heuristic tests", "Whether the transaction hex is a serialized witness transaction \n"
34083478
" If iswitness is not present, heuristic tests will be used in decoding"},
3479+
{"solving_data", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "Keys and scripts needed for producing a final transaction with a dummy signature. Used for fee estimation during coin selection.\n",
3480+
{
3481+
{"pubkeys", RPCArg::Type::ARR, /* default */ "empty array", "A json array of public keys.\n",
3482+
{
3483+
{"pubkey", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A public key"},
3484+
},
3485+
},
3486+
{"scripts", RPCArg::Type::ARR, /* default */ "empty array", "A json array of scripts.\n",
3487+
{
3488+
{"script", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A script"},
3489+
},
3490+
},
3491+
{"descriptors", RPCArg::Type::ARR, /* default */ "empty array", "A json array of descriptors.\n",
3492+
{
3493+
{"descriptor", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A descriptor"},
3494+
},
3495+
}
3496+
}
3497+
},
34093498
},
34103499
RPCResult{
34113500
"{\n"
@@ -3438,7 +3527,7 @@ static UniValue fundrawtransaction(const JSONRPCRequest& request)
34383527

34393528
CAmount fee;
34403529
int change_position;
3441-
FundTransaction(pwallet, tx, fee, change_position, request.params[1]);
3530+
FundTransaction(pwallet, tx, fee, change_position, request.params[1], request.params[3]);
34423531

34433532
UniValue result(UniValue::VOBJ);
34443533
result.pushKV("hex", EncodeHexTx(CTransaction(tx)));
@@ -4581,7 +4670,7 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
45814670
return NullUniValue;
45824671
}
45834672

4584-
if (request.fHelp || request.params.size() < 2 || request.params.size() > 5)
4673+
if (request.fHelp || request.params.size() < 2 || request.params.size() > 6)
45854674
throw std::runtime_error(
45864675
RPCHelpMan{"walletcreatefundedpsbt",
45874676
"\nCreates and funds a transaction in the Partially Signed Transaction format. Inputs will be added if supplied inputs are not enough\n"
@@ -4594,6 +4683,9 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
45944683
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"},
45954684
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"},
45964685
{"sequence", RPCArg::Type::NUM, RPCArg::Optional::NO, "The sequence number"},
4686+
{"pegin_bitcoin_tx", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The raw bitcoin transaction (in hex) depositing bitcoin to the mainchain_address generated by getpeginaddress"},
4687+
{"pegin_txout_proof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "A rawtxoutproof (in hex) generated by the mainchain daemon's `gettxoutproof` containing a proof of only bitcoin_tx"},
4688+
{"pegin_claim_script", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The witness program generated by getpeginaddress."},
45974689
},
45984690
},
45994691
},
@@ -4642,6 +4734,25 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
46424734
},
46434735
"options"},
46444736
{"bip32derivs", RPCArg::Type::BOOL, /* default */ "false", "If true, includes the BIP 32 derivation paths for public keys if we know them"},
4737+
{"solving_data", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "Keys and scripts needed for producing a final transaction with a dummy signature. Used for fee estimation during coin selection.\n",
4738+
{
4739+
{"pubkeys", RPCArg::Type::ARR, /* default */ "empty array", "A json array of public keys.\n",
4740+
{
4741+
{"pubkey", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A public key"},
4742+
},
4743+
},
4744+
{"scripts", RPCArg::Type::ARR, /* default */ "empty array", "A json array of scripts.\n",
4745+
{
4746+
{"script", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A script"},
4747+
},
4748+
},
4749+
{"descriptors", RPCArg::Type::ARR, /* default */ "empty array", "A json array of descriptors.\n",
4750+
{
4751+
{"descriptor", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A descriptor"},
4752+
},
4753+
}
4754+
}
4755+
},
46454756
},
46464757
RPCResult{
46474758
"{\n"
@@ -4672,8 +4783,8 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
46724783
// It's hard to control the behavior of FundTransaction, so we will wait
46734784
// until after it's done, then extract the blinding keys from the output
46744785
// nonces.
4675-
CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], request.params[3]["replaceable"], NullUniValue /* CA: assets_in */, nullptr /* output_pubkeys_out */, false /* allow_peg_in */);
4676-
FundTransaction(pwallet, rawTx, fee, change_position, request.params[3]);
4786+
CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], request.params[3]["replaceable"], NullUniValue /* CA: assets_in */, nullptr /* output_pubkeys_out */, true /* allow_peg_in */);
4787+
FundTransaction(pwallet, rawTx, fee, change_position, request.params[3], request.params[5]);
46774788

46784789
// Make a blank psbt
46794790
PartiallySignedTransaction psbtx(rawTx);
@@ -4693,6 +4804,42 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
46934804
throw JSONRPCTransactionError(err);
46944805
}
46954806

4807+
// Add peg-in stuff if it's there
4808+
for (unsigned int i = 0; i < rawTx.vin.size(); ++i) {
4809+
if (psbtx.tx->vin[i].m_is_pegin) {
4810+
CScriptWitness& pegin_witness = psbtx.tx->witness.vtxinwit[i].m_pegin_witness;
4811+
CAmount val;
4812+
VectorReader vr_val(SER_NETWORK, PROTOCOL_VERSION, pegin_witness.stack[0], 0);
4813+
vr_val >> val;
4814+
psbtx.inputs[i].value = val;
4815+
VectorReader vr_asset(SER_NETWORK, PROTOCOL_VERSION, pegin_witness.stack[1], 0);
4816+
vr_asset >> psbtx.inputs[i].asset;
4817+
VectorReader vr_genesis(SER_NETWORK, PROTOCOL_VERSION, pegin_witness.stack[2], 0);
4818+
vr_genesis >> psbtx.inputs[i].genesis_hash;
4819+
psbtx.inputs[i].claim_script.assign(pegin_witness.stack[3].begin(), pegin_witness.stack[3].end());
4820+
4821+
VectorReader vr_tx(SER_NETWORK, PROTOCOL_VERSION, pegin_witness.stack[4], 0);
4822+
VectorReader vr_proof(SER_NETWORK, PROTOCOL_VERSION, pegin_witness.stack[5], 0);
4823+
if (Params().GetConsensus().ParentChainHasPow()) {
4824+
Sidechain::Bitcoin::CTransactionRef tx_btc;
4825+
vr_tx >> tx_btc;
4826+
psbtx.inputs[i].peg_in_tx = tx_btc;
4827+
Sidechain::Bitcoin::CMerkleBlock tx_proof;
4828+
vr_proof >> tx_proof;
4829+
psbtx.inputs[i].txout_proof = tx_proof;
4830+
} else {
4831+
CTransactionRef tx_btc;
4832+
vr_tx >> tx_btc;
4833+
psbtx.inputs[i].peg_in_tx = tx_btc;
4834+
CMerkleBlock tx_proof;
4835+
vr_proof >> tx_proof;
4836+
psbtx.inputs[i].txout_proof = tx_proof;
4837+
}
4838+
pegin_witness.SetNull();
4839+
psbtx.tx->vin[i].m_is_pegin = false;
4840+
}
4841+
}
4842+
46964843
// Serialize the PSBT
46974844
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
46984845
ssTx << psbtx;
@@ -6567,7 +6714,7 @@ static const CRPCCommand commands[] =
65676714
// --------------------- ------------------------ ----------------------- ----------
65686715
{ "generating", "generate", &generate, {"nblocks","maxtries"} },
65696716
{ "hidden", "resendwallettransactions", &resendwallettransactions, {} },
6570-
{ "rawtransactions", "fundrawtransaction", &fundrawtransaction, {"hexstring","options","iswitness"} },
6717+
{ "rawtransactions", "fundrawtransaction", &fundrawtransaction, {"hexstring","options","iswitness","solving_data"} },
65716718
{ "wallet", "abandontransaction", &abandontransaction, {"txid"} },
65726719
{ "wallet", "abortrescan", &abortrescan, {} },
65736720
{ "wallet", "addmultisigaddress", &addmultisigaddress, {"nrequired","keys","label","address_type"} },
@@ -6616,7 +6763,7 @@ static const CRPCCommand commands[] =
66166763
{ "wallet", "signmessage", &signmessage, {"address","message"} },
66176764
{ "wallet", "signrawtransactionwithwallet", &signrawtransactionwithwallet, {"hexstring","prevtxs","sighashtype"} },
66186765
{ "wallet", "unloadwallet", &unloadwallet, {"wallet_name"} },
6619-
{ "wallet", "walletcreatefundedpsbt", &walletcreatefundedpsbt, {"inputs","outputs","locktime","options","bip32derivs"} },
6766+
{ "wallet", "walletcreatefundedpsbt", &walletcreatefundedpsbt, {"inputs","outputs","locktime","options","bip32derivs","solving_data"} },
66206767
{ "wallet", "walletlock", &walletlock, {} },
66216768
{ "wallet", "walletpassphrase", &walletpassphrase, {"passphrase","timeout"} },
66226769
{ "wallet", "walletpassphrasechange", &walletpassphrasechange, {"oldpassphrase","newpassphrase"} },

0 commit comments

Comments
 (0)