Skip to content

Commit

Permalink
Merge #755: Be able to fund transactions with peg-ins specified
Browse files Browse the repository at this point in the history
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
  • Loading branch information
instagibbs committed Dec 4, 2019
2 parents 2949ea9 + 00eb6c0 commit 526e802
Show file tree
Hide file tree
Showing 10 changed files with 320 additions and 44 deletions.
2 changes: 2 additions & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,13 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "combinerawtransaction", 0, "txs" },
{ "fundrawtransaction", 1, "options" },
{ "fundrawtransaction", 2, "iswitness" },
{ "fundrawtransaction", 3, "solving_data" },
{ "walletcreatefundedpsbt", 0, "inputs" },
{ "walletcreatefundedpsbt", 1, "outputs" },
{ "walletcreatefundedpsbt", 2, "locktime" },
{ "walletcreatefundedpsbt", 3, "options" },
{ "walletcreatefundedpsbt", 4, "bip32derivs" },
{ "walletcreatefundedpsbt", 5, "solving_data" },
{ "walletprocesspsbt", 1, "sign" },
{ "walletprocesspsbt", 3, "bip32derivs" },
{ "walletfillpsbtdata", 1, "bip32derivs" },
Expand Down
2 changes: 2 additions & 0 deletions src/wallet/coincontrol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ void CCoinControl::SetNull()
m_confirm_target.reset();
m_signal_bip125_rbf.reset();
m_fee_mode = FeeEstimateMode::UNSET;
m_external_txouts.clear();
m_external_provider = FlatSigningProvider();
}

34 changes: 34 additions & 0 deletions src/wallet/coincontrol.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class CCoinControl
bool m_avoid_partial_spends;
//! Fee estimation mode to control arguments to estimateSmartFee
FeeEstimateMode m_fee_mode;
//! SigningProvider that has pubkeys and scripts to do spend size estimation for external inputs
FlatSigningProvider m_external_provider;

CCoinControl()
{
Expand All @@ -55,11 +57,42 @@ class CCoinControl
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)
{
setSelected.insert(output);
}

void SelectExternal(const COutPoint& outpoint, const CTxOut& txout)
{
setSelected.insert(outpoint);
m_external_txouts.emplace(outpoint, txout);
}

void Select(const COutPoint& outpoint, const Sidechain::Bitcoin::CTxOut& txout_in)
{
setSelected.insert(outpoint);
CTxOut txout;
txout.scriptPubKey = txout_in.scriptPubKey;
txout.nValue.SetToAmount(txout_in.nValue);
txout.nAsset.SetToAsset(Params().GetConsensus().pegged_asset);
m_external_txouts.emplace(outpoint, txout);
}

void UnSelect(const COutPoint& output)
{
setSelected.erase(output);
Expand All @@ -77,6 +110,7 @@ class CCoinControl

private:
std::set<COutPoint> setSelected;
std::map<COutPoint, CTxOut> m_external_txouts;
};

#endif // BITCOIN_WALLET_COINCONTROL_H
37 changes: 37 additions & 0 deletions src/wallet/coinselection.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
#define BITCOIN_WALLET_COINSELECTION_H

#include <amount.h>
#include <chainparams.h>
#include <primitives/transaction.h>
#include <primitives/bitcoin/transaction.h>
#include <random.h>

//! target minimum change amount
Expand All @@ -26,6 +28,41 @@ class CInputCoin {
m_input_bytes = input_bytes;
}

CInputCoin(const COutPoint& outpoint_in, const CTxOut& txout_in)
{
outpoint = outpoint_in;
txout = txout_in;
if (txout.nValue.IsExplicit()) {
effective_value = txout_in.nValue.GetAmount();
value = txout.nValue.GetAmount();
asset = txout.nAsset.GetAsset();
} else {
effective_value = 0;
}
}

CInputCoin(const COutPoint& outpoint_in, const CTxOut& txout_in, int input_bytes) : CInputCoin(outpoint_in, txout_in)
{
m_input_bytes = input_bytes;
}

CInputCoin(const COutPoint& outpoint_in, const Sidechain::Bitcoin::CTxOut& txout_in)
{
outpoint = outpoint_in;
effective_value = txout_in.nValue;
txout.SetNull();
txout.scriptPubKey = txout_in.scriptPubKey;
txout.nValue.SetToAmount(txout_in.nValue);
txout.nAsset.SetToAsset(Params().GetConsensus().pegged_asset);
asset = Params().GetConsensus().pegged_asset;
value = txout_in.nValue;
}

CInputCoin(const COutPoint& outpoint_in, const Sidechain::Bitcoin::CTxOut& txout_in, int input_bytes) : CInputCoin(outpoint_in, txout_in)
{
m_input_bytes = input_bytes;
}

COutPoint outpoint;
CTxOut txout;
CAmount effective_value;
Expand Down
163 changes: 155 additions & 8 deletions src/wallet/rpcwallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3212,7 +3212,7 @@ static UniValue listunspent(const JSONRPCRequest& request)
return results;
}

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

if (!solving_data.isNull()) {
if (solving_data.exists("pubkeys")) {
UniValue pubkey_strs = solving_data["pubkeys"].get_array();
for (unsigned int i = 0; i < pubkey_strs.size(); ++i) {
std::vector<unsigned char> data(ParseHex(pubkey_strs[i].get_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", pubkey_strs[i].get_str()));
}
coinControl.m_external_provider.pubkeys.emplace(pubkey.GetID(), pubkey);
// Add witnes script for pubkeys
CScript wit_script = GetScriptForDestination(WitnessV0KeyHash(pubkey.GetID()));
coinControl.m_external_provider.scripts.emplace(CScriptID(wit_script), wit_script);
}
}

if (solving_data.exists("scripts")) {
UniValue script_strs = solving_data["scripts"].get_array();
for (unsigned int i = 0; i < script_strs.size(); ++i) {
CScript script = ParseScript(script_strs[i].get_str());
coinControl.m_external_provider.scripts.emplace(CScriptID(script), script);
}
}

if (solving_data.exists("descriptors")) {
UniValue desc_strs = solving_data["descriptors"].get_array();
for (unsigned int i = 0; i < desc_strs.size(); ++i) {
FlatSigningProvider desc_out;
std::unique_ptr<Descriptor> desc = Parse(desc_strs[i].get_str(), desc_out, true);
coinControl.m_external_provider = Merge(coinControl.m_external_provider, desc_out);
}
}
}

if (tx.vout.size() == 0)
throw JSONRPCError(RPC_INVALID_PARAMETER, "TX must have at least one output");

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

// Check any existing inputs for peg-in data and add to external txouts if so
// Fetch specified UTXOs from the UTXO set
const auto& fedpegscripts = GetValidFedpegScripts(chainActive.Tip(), Params().GetConsensus(), true /* nextblock_validation */);
std::map<COutPoint, Coin> coins;
for (unsigned int i = 0; i < tx.vin.size(); ++i ) {
const CTxIn& txin = tx.vin[i];
coins[txin.prevout]; // Create empty map entry keyed by prevout.
if (txin.m_is_pegin) {
std::string err;
if (tx.witness.vtxinwit.size() != tx.vin.size() || !IsValidPeginWitness(tx.witness.vtxinwit[i].m_pegin_witness, fedpegscripts, txin.prevout, err, false)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Transaction contains invalid peg-in input: %s", err));
}
CScriptWitness& pegin_witness = tx.witness.vtxinwit[i].m_pegin_witness;
CTxOut txout = GetPeginOutputFromWitness(pegin_witness);
coinControl.SelectExternal(txin.prevout, txout);
}
}
CCoinsView viewDummy;
CCoinsViewCache view(&viewDummy);
{
LOCK2(cs_main, mempool.cs);
CCoinsViewCache& chain_view = *pcoinsTip;
CCoinsViewMemPool mempool_view(&chain_view, mempool);
for (auto& coin : coins) {
if (!mempool_view.GetCoin(coin.first, coin.second)) {
// Either the coin is not in the CCoinsViewCache or is spent. Clear it.
coin.second.Clear();
}
}
}
for (const auto& coin : coins) {
if (!coin.second.out.IsNull()) {
coinControl.SelectExternal(coin.first, coin.second.out);
}
}

std::string strFailReason;

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

if (request.fHelp || request.params.size() < 1 || request.params.size() > 3)
if (request.fHelp || request.params.size() < 1 || request.params.size() > 4)
throw std::runtime_error(
RPCHelpMan{"fundrawtransaction",
"\nAdd inputs to a transaction until it has enough in value to meet its out value.\n"
Expand Down Expand Up @@ -3406,6 +3476,25 @@ static UniValue fundrawtransaction(const JSONRPCRequest& request)
"options"},
{"iswitness", RPCArg::Type::BOOL, /* default */ "depends on heuristic tests", "Whether the transaction hex is a serialized witness transaction \n"
" If iswitness is not present, heuristic tests will be used in decoding"},
{"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",
{
{"pubkeys", RPCArg::Type::ARR, /* default */ "empty array", "A json array of public keys.\n",
{
{"pubkey", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A public key"},
},
},
{"scripts", RPCArg::Type::ARR, /* default */ "empty array", "A json array of scripts.\n",
{
{"script", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A script"},
},
},
{"descriptors", RPCArg::Type::ARR, /* default */ "empty array", "A json array of descriptors.\n",
{
{"descriptor", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A descriptor"},
},
}
}
},
},
RPCResult{
"{\n"
Expand Down Expand Up @@ -3438,7 +3527,7 @@ static UniValue fundrawtransaction(const JSONRPCRequest& request)

CAmount fee;
int change_position;
FundTransaction(pwallet, tx, fee, change_position, request.params[1]);
FundTransaction(pwallet, tx, fee, change_position, request.params[1], request.params[3]);

UniValue result(UniValue::VOBJ);
result.pushKV("hex", EncodeHexTx(CTransaction(tx)));
Expand Down Expand Up @@ -4581,7 +4670,7 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
return NullUniValue;
}

if (request.fHelp || request.params.size() < 2 || request.params.size() > 5)
if (request.fHelp || request.params.size() < 2 || request.params.size() > 6)
throw std::runtime_error(
RPCHelpMan{"walletcreatefundedpsbt",
"\nCreates and funds a transaction in the Partially Signed Transaction format. Inputs will be added if supplied inputs are not enough\n"
Expand All @@ -4594,6 +4683,9 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"},
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"},
{"sequence", RPCArg::Type::NUM, RPCArg::Optional::NO, "The sequence number"},
{"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"},
{"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"},
{"pegin_claim_script", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The witness program generated by getpeginaddress."},
},
},
},
Expand Down Expand Up @@ -4642,6 +4734,25 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
},
"options"},
{"bip32derivs", RPCArg::Type::BOOL, /* default */ "false", "If true, includes the BIP 32 derivation paths for public keys if we know them"},
{"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",
{
{"pubkeys", RPCArg::Type::ARR, /* default */ "empty array", "A json array of public keys.\n",
{
{"pubkey", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A public key"},
},
},
{"scripts", RPCArg::Type::ARR, /* default */ "empty array", "A json array of scripts.\n",
{
{"script", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A script"},
},
},
{"descriptors", RPCArg::Type::ARR, /* default */ "empty array", "A json array of descriptors.\n",
{
{"descriptor", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A descriptor"},
},
}
}
},
},
RPCResult{
"{\n"
Expand Down Expand Up @@ -4672,8 +4783,8 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
// It's hard to control the behavior of FundTransaction, so we will wait
// until after it's done, then extract the blinding keys from the output
// nonces.
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 */);
FundTransaction(pwallet, rawTx, fee, change_position, request.params[3]);
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 */);
FundTransaction(pwallet, rawTx, fee, change_position, request.params[3], request.params[5]);

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

// Add peg-in stuff if it's there
for (unsigned int i = 0; i < rawTx.vin.size(); ++i) {
if (psbtx.tx->vin[i].m_is_pegin) {
CScriptWitness& pegin_witness = psbtx.tx->witness.vtxinwit[i].m_pegin_witness;
CAmount val;
VectorReader vr_val(SER_NETWORK, PROTOCOL_VERSION, pegin_witness.stack[0], 0);
vr_val >> val;
psbtx.inputs[i].value = val;
VectorReader vr_asset(SER_NETWORK, PROTOCOL_VERSION, pegin_witness.stack[1], 0);
vr_asset >> psbtx.inputs[i].asset;
VectorReader vr_genesis(SER_NETWORK, PROTOCOL_VERSION, pegin_witness.stack[2], 0);
vr_genesis >> psbtx.inputs[i].genesis_hash;
psbtx.inputs[i].claim_script.assign(pegin_witness.stack[3].begin(), pegin_witness.stack[3].end());

VectorReader vr_tx(SER_NETWORK, PROTOCOL_VERSION, pegin_witness.stack[4], 0);
VectorReader vr_proof(SER_NETWORK, PROTOCOL_VERSION, pegin_witness.stack[5], 0);
if (Params().GetConsensus().ParentChainHasPow()) {
Sidechain::Bitcoin::CTransactionRef tx_btc;
vr_tx >> tx_btc;
psbtx.inputs[i].peg_in_tx = tx_btc;
Sidechain::Bitcoin::CMerkleBlock tx_proof;
vr_proof >> tx_proof;
psbtx.inputs[i].txout_proof = tx_proof;
} else {
CTransactionRef tx_btc;
vr_tx >> tx_btc;
psbtx.inputs[i].peg_in_tx = tx_btc;
CMerkleBlock tx_proof;
vr_proof >> tx_proof;
psbtx.inputs[i].txout_proof = tx_proof;
}
pegin_witness.SetNull();
psbtx.tx->vin[i].m_is_pegin = false;
}
}

// Serialize the PSBT
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx;
Expand Down Expand Up @@ -6567,7 +6714,7 @@ static const CRPCCommand commands[] =
// --------------------- ------------------------ ----------------------- ----------
{ "generating", "generate", &generate, {"nblocks","maxtries"} },
{ "hidden", "resendwallettransactions", &resendwallettransactions, {} },
{ "rawtransactions", "fundrawtransaction", &fundrawtransaction, {"hexstring","options","iswitness"} },
{ "rawtransactions", "fundrawtransaction", &fundrawtransaction, {"hexstring","options","iswitness","solving_data"} },
{ "wallet", "abandontransaction", &abandontransaction, {"txid"} },
{ "wallet", "abortrescan", &abortrescan, {} },
{ "wallet", "addmultisigaddress", &addmultisigaddress, {"nrequired","keys","label","address_type"} },
Expand Down Expand Up @@ -6616,7 +6763,7 @@ static const CRPCCommand commands[] =
{ "wallet", "signmessage", &signmessage, {"address","message"} },
{ "wallet", "signrawtransactionwithwallet", &signrawtransactionwithwallet, {"hexstring","prevtxs","sighashtype"} },
{ "wallet", "unloadwallet", &unloadwallet, {"wallet_name"} },
{ "wallet", "walletcreatefundedpsbt", &walletcreatefundedpsbt, {"inputs","outputs","locktime","options","bip32derivs"} },
{ "wallet", "walletcreatefundedpsbt", &walletcreatefundedpsbt, {"inputs","outputs","locktime","options","bip32derivs","solving_data"} },
{ "wallet", "walletlock", &walletlock, {} },
{ "wallet", "walletpassphrase", &walletpassphrase, {"passphrase","timeout"} },
{ "wallet", "walletpassphrasechange", &walletpassphrasechange, {"oldpassphrase","newpassphrase"} },
Expand Down
Loading

0 comments on commit 526e802

Please sign in to comment.