From 698340b62adbc24075e7c818844206a6582c6300 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 18 Oct 2019 14:44:58 -0400 Subject: [PATCH 1/7] Allow CInputCoin to also be constructed with COutPoint and CTxOut --- src/wallet/coinselection.h | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/wallet/coinselection.h b/src/wallet/coinselection.h index 0fee6ada968..ebee172223c 100644 --- a/src/wallet/coinselection.h +++ b/src/wallet/coinselection.h @@ -6,7 +6,9 @@ #define BITCOIN_WALLET_COINSELECTION_H #include +#include #include +#include #include //! target minimum change amount @@ -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; From 8258aaa7338948d96c812e7505532f53380ebda4 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 22 Oct 2019 15:06:24 -0400 Subject: [PATCH 2/7] Allow Coin Selection be able to take external inputs --- src/wallet/coincontrol.cpp | 2 ++ src/wallet/coincontrol.h | 24 +++++++++++++ src/wallet/wallet.cpp | 69 ++++++++++++++++++++++++++------------ src/wallet/wallet.h | 13 +++---- 4 files changed, 79 insertions(+), 29 deletions(-) diff --git a/src/wallet/coincontrol.cpp b/src/wallet/coincontrol.cpp index 6b3090d02ce..0c190b7a2c5 100644 --- a/src/wallet/coincontrol.cpp +++ b/src/wallet/coincontrol.cpp @@ -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(); } diff --git a/src/wallet/coincontrol.h b/src/wallet/coincontrol.h index b9aeb7b08f6..0041852ec9f 100644 --- a/src/wallet/coincontrol.h +++ b/src/wallet/coincontrol.h @@ -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() { @@ -55,11 +57,32 @@ 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 UnSelect(const COutPoint& output) { setSelected.erase(output); @@ -77,6 +100,7 @@ class CCoinControl private: std::set setSelected; + std::map m_external_txouts; }; #endif // BITCOIN_WALLET_COINCONTROL_H diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index bac5164134a..ea2fa432a3b 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1626,13 +1626,13 @@ int64_t CWalletTx::GetTxTime() const // 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 -bool CWallet::DummySignInput(CMutableTransaction& tx, const size_t nIn, const CTxOut& txout, bool use_max_sig) const +static bool DummySignInput(const SigningProvider* provider, CMutableTransaction& tx, const size_t nIn, const CTxOut& txout, bool use_max_sig) { // Fill in dummy signatures for fee calculation. const CScript& scriptPubKey = txout.scriptPubKey; SignatureData sigdata; - if (!ProduceSignature(*this, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, scriptPubKey, sigdata)) { + if (!ProduceSignature(*provider, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, scriptPubKey, sigdata)) { return false; } UpdateTransaction(tx, nIn, sigdata); @@ -1640,14 +1640,18 @@ bool CWallet::DummySignInput(CMutableTransaction& tx, const size_t nIn, const CT } // Helper for producing a bunch of max-sized low-S low-R signatures (eg 71 bytes) -bool CWallet::DummySignTx(CMutableTransaction &txNew, const std::vector &txouts, bool use_max_sig) const +bool CWallet::DummySignTx(CMutableTransaction &txNew, const std::vector &txouts, const CCoinControl* coin_control) const { // Fill in dummy signatures for fee calculation. int nIn = 0; for (const auto& txout : txouts) { - if (!DummySignInput(txNew, nIn, txout, use_max_sig)) { - return false; + // Use max sig if watch only inputs were used or if this particular input is an external input + bool use_max_sig = coin_control && (coin_control->fAllowWatchOnly || (coin_control && coin_control->IsExternalSelected(txNew.vin[nIn].prevout))); + if (!DummySignInput(this, txNew, nIn, txout, use_max_sig)) { + if (!coin_control || !DummySignInput(&coin_control->m_external_provider, txNew, nIn, txout, use_max_sig)) { + return false; + } } nIn++; @@ -1655,28 +1659,33 @@ bool CWallet::DummySignTx(CMutableTransaction &txNew, const std::vector return true; } -int64_t CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, bool use_max_sig) +int64_t CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, const CCoinControl* coin_control) { std::vector txouts; - // Look up the inputs. We should have already checked that this transaction - // IsAllFromMe(ISMINE_SPENDABLE), so every input should already be in our - // wallet, with a valid index into the vout array, and the ability to sign. + // Look up the inputs. The inputs are either in the wallet, or in coin_control. for (const CTxIn& input : tx.vin) { const auto mi = wallet->mapWallet.find(input.prevout.hash); - 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[input.prevout.n]); + } else if (coin_control) { + CTxOut txout; + if (!coin_control->GetExternalOutput(input.prevout, txout)) { + return -1; + } + txouts.emplace_back(txout); + } else { return -1; } - assert(input.prevout.n < mi->second.tx->vout.size()); - txouts.emplace_back(mi->second.tx->vout[input.prevout.n]); } - return CalculateMaximumSignedTxSize(tx, wallet, txouts, use_max_sig); + return CalculateMaximumSignedTxSize(tx, wallet, txouts, coin_control); } // txouts needs to be in the order of tx.vin -int64_t CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, const std::vector& txouts, bool use_max_sig) +int64_t CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, const std::vector& txouts, const CCoinControl* coin_control) { CMutableTransaction txNew(tx); - if (!wallet->DummySignTx(txNew, txouts, use_max_sig)) { + if (!wallet->DummySignTx(txNew, txouts, coin_control)) { // This should never happen, because IsAllFromMe(ISMINE_SPENDABLE) // implies that we can sign for every input. return -1; @@ -1688,7 +1697,7 @@ int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* wallet, { CMutableTransaction txn; txn.vin.push_back(CTxIn(COutPoint())); - if (!wallet->DummySignInput(txn, 0, txout, use_max_sig)) { + if (!DummySignInput(wallet, txn, 0, txout, use_max_sig)) { // This should never happen, because IsAllFromMe(ISMINE_SPENDABLE) // implies that we can sign for every input. return -1; @@ -2690,8 +2699,18 @@ bool CWallet::SelectCoins(const std::vector& vAvailableCoins, const CAm } mapValueFromPresetInputs[pcoin->GetOutputAsset(outpoint.n)] += amt; setPresetCoins.insert(CInputCoin(pcoin, outpoint.n)); - } else - return false; // TODO: Allow non-wallet inputs + } else { + CTxOut txout; + if (coin_control.GetExternalOutput(outpoint, txout)) { + if (!txout.nValue.IsExplicit() || !txout.nAsset.IsExplicit()) { + return false; // We can't get its value, so abort + } + mapValueFromPresetInputs[txout.nAsset.GetAsset()] += txout.nValue.GetAmount(); + setPresetCoins.insert(CInputCoin(outpoint, txout)); + } else { + return false; + } + } } // remove preset inputs from vCoins @@ -2824,8 +2843,11 @@ bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, int& nC coinControl.ListSelected(vPresetInputs); for (const COutPoint& presetInput : vPresetInputs) { std::map::const_iterator it = mapWallet.find(presetInput.hash); + CTxOut txout; if (it != mapWallet.end()) { setAssets.insert(it->second.GetOutputAsset(presetInput.n)); + } else if (coinControl.GetExternalOutput(presetInput, txout)) { + setAssets.insert(txout.nAsset.GetAsset()); } } @@ -3162,13 +3184,18 @@ bool CWallet::CreateTransaction(interfaces::Chain::Lock& locked_chain, const std std::vector vPresetInputs; coin_control.ListSelected(vPresetInputs); for (const COutPoint& presetInput : vPresetInputs) { + CAsset asset; std::map::const_iterator it = mapWallet.find(presetInput.hash); - if (it == mapWallet.end()) { + CTxOut txout; + if (it != mapWallet.end()) { + asset = it->second.GetOutputAsset(presetInput.n); + } else if (coin_control.GetExternalOutput(presetInput, txout)) { + asset = txout.nAsset.GetAsset(); + } else { // Ignore this here, will fail more gracefully later. continue; } - CAsset asset = it->second.GetOutputAsset(presetInput.n); if (mapScriptChange.find(asset) != mapScriptChange.end()) { // This asset already has a change script. continue; @@ -3509,7 +3536,7 @@ bool CWallet::CreateTransaction(interfaces::Chain::Lock& locked_chain, const std } } - nBytes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, coin_control.fAllowWatchOnly); + nBytes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, &coin_control); if (nBytes < 0) { strFailReason = _("Signing transaction failed"); return false; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 4262744a9ac..fd0ce2792b3 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1084,14 +1084,13 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface std::string& strFailReason, const CCoinControl& coin_control, bool sign = true, BlindDetails* blind_details = nullptr, const IssuanceDetails* issuance_details = nullptr); bool CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::vector> orderForm, std::vector>& reservekey, CConnman* connman, CValidationState& state, const BlindDetails* blind_details = nullptr); - bool DummySignTx(CMutableTransaction &txNew, const std::set &txouts, bool use_max_sig = false) const + bool DummySignTx(CMutableTransaction &txNew, const std::set &txouts, const CCoinControl* coin_control = nullptr) const { std::vector v_txouts(txouts.size()); 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 &txouts, bool use_max_sig = false) const; - bool DummySignInput(CMutableTransaction &tx, const size_t nIn, const CTxOut &txout, bool use_max_sig = false) const; + bool DummySignTx(CMutableTransaction &txNew, const std::vector &txouts, const CCoinControl* coin_control = nullptr) const; CFeeRate m_pay_tx_fee{DEFAULT_PAY_TX_FEE}; unsigned int m_confirm_target{DEFAULT_TX_CONFIRM_TARGET}; @@ -1445,8 +1444,6 @@ class WalletRescanReserver // Calculate the size of the transaction assuming all signatures are max size // Use DummySignatureCreator, which inserts 71 byte signatures everywhere. -// NOTE: this requires that all inputs must be in mapWallet (eg the tx should -// be IsAllFromMe). -int64_t CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, bool use_max_sig = false) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet); -int64_t CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, const std::vector& txouts, bool use_max_sig = false); +int64_t CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, const CCoinControl* coin_control = nullptr) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet); +int64_t CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *wallet, const std::vector& txouts, const CCoinControl* coin_control = nullptr); #endif // BITCOIN_WALLET_WALLET_H From 9486cdf5491c963e0ba2fc96294c6d4b4619626b Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 18 Oct 2019 17:54:03 -0400 Subject: [PATCH 3/7] Give a better error when tx size estimation fails --- src/wallet/wallet.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index ea2fa432a3b..ae5a160c99a 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3538,7 +3538,7 @@ bool CWallet::CreateTransaction(interfaces::Chain::Lock& locked_chain, const std nBytes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, &coin_control); if (nBytes < 0) { - strFailReason = _("Signing transaction failed"); + strFailReason = _("Missing solving data for estimating transaction size"); return false; } From c25874672bdebee0f682f28fe92d20c5b61010e3 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 18 Oct 2019 19:43:01 -0400 Subject: [PATCH 4/7] allow fundtx rpcs to work with external inputs --- src/rpc/client.cpp | 2 + src/wallet/rpcwallet.cpp | 110 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 105 insertions(+), 7 deletions(-) diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 4e700beb372..c48c958930b 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -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" }, diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 9148bae7512..8afa6cad0e6 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -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 @@ -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 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 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"); @@ -3347,6 +3381,30 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f setSubtractFeeFromOutputs.insert(pos); } + // Fetch specified UTXOs from the UTXO set + std::map coins; + for (const CTxIn& txin : tx.vin) { + coins[txin.prevout]; // Create empty map entry keyed by prevout. + } + 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)) { @@ -3363,7 +3421,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" @@ -3406,6 +3464,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" @@ -3438,7 +3515,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))); @@ -4581,7 +4658,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" @@ -4642,6 +4719,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" @@ -4673,7 +4769,7 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request) // 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]); + FundTransaction(pwallet, rawTx, fee, change_position, request.params[3], request.params[5]); // Make a blank psbt PartiallySignedTransaction psbtx(rawTx); @@ -6567,7 +6663,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"} }, @@ -6616,7 +6712,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"} }, From 74c4d37788ff078fda78bd79379e37c3f7317d0d Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 22 Oct 2019 17:54:34 -0400 Subject: [PATCH 5/7] Tests for fundrawtransaction with external inputs --- test/functional/rpc_fundrawtransaction.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py index d198bd5e143..f3b8c440f2f 100755 --- a/test/functional/rpc_fundrawtransaction.py +++ b/test/functional/rpc_fundrawtransaction.py @@ -777,5 +777,26 @@ def run_test(self): # the total subtracted from the outputs is equal to the fee assert_equal(share[0] + share[2] + share[3], result[0]['fee']) + n0_blind_addr = self.nodes[0].getnewaddress() + addr_info = self.nodes[0].getaddressinfo(n0_blind_addr) + txid = self.nodes[2].sendtoaddress(addr_info['unconfidential'], 10) + self.sync_all() + vout = find_vout_for_address(self.nodes[0], txid, n0_blind_addr) + self.nodes[0].generate(1) + self.sync_all() + + # An external input without solving data should result in an error + raw_tx = self.nodes[2].createrawtransaction([{"txid": txid, "vout": vout}], {addr_info['unconfidential']: 20}) + assert_raises_rpc_error(-4, "Missing solving data for estimating transaction size", self.nodes[2].fundrawtransaction, raw_tx) + + # But funding should work when the solving data is provided + funded_tx = self.nodes[2].fundrawtransaction(raw_tx, {}, False, {"pubkeys": [addr_info['pubkey']]}) + signed_tx = self.nodes[2].signrawtransactionwithwallet(funded_tx['hex']) + assert not signed_tx['complete'] + signed_tx = self.nodes[0].signrawtransactionwithwallet(signed_tx['hex']) + assert signed_tx['complete'] + # Don't send because we didn't blind it so it's not actually valid. + # self.nodes[0].sendrawtransaction(signed_tx['hex']) + if __name__ == '__main__': RawTransactionsTest().main() From 1ce74e07f35452e6c4512bbdfa1cecf5bc064f6b Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 22 Oct 2019 18:32:03 -0400 Subject: [PATCH 6/7] Be able to fund transactions with peg-ins --- src/wallet/coincontrol.h | 10 +++++++ src/wallet/rpcwallet.cpp | 55 +++++++++++++++++++++++++++++++++++-- test/functional/rpc_psbt.py | 5 ---- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/wallet/coincontrol.h b/src/wallet/coincontrol.h index 0041852ec9f..7ee3a8d72bc 100644 --- a/src/wallet/coincontrol.h +++ b/src/wallet/coincontrol.h @@ -83,6 +83,16 @@ class CCoinControl 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); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 8afa6cad0e6..b2485651b57 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -3381,10 +3381,22 @@ 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 coins; - for (const CTxIn& txin : tx.vin) { + 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); @@ -4671,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."}, }, }, }, @@ -4768,7 +4783,7 @@ 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 */); + 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 @@ -4789,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; diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 11a0631721a..7a0f9f6d0f1 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -524,11 +524,6 @@ def run_test(self): # Some Confidential-Assets-specific tests self.run_ca_tests() - # Check that peg-ins are disallowed for walletcreatefundedpsbt - assert_raises_rpc_error(-8, 'pegin_ arguments provided but this command does not support peg-ins', self.nodes[0].walletcreatefundedpsbt, [{"txid": "0000000000000000000000000000000000000000000000000000000000000000", "vout": 0, "pegin_bitcoin_tx": "00"}], [{self.nodes[0].getnewaddress(): 1}]) - assert_raises_rpc_error(-8, 'pegin_ arguments provided but this command does not support peg-ins', self.nodes[0].walletcreatefundedpsbt, [{"txid": "0000000000000000000000000000000000000000000000000000000000000000", "vout": 0, "pegin_txout_proof": "00"}], [{self.nodes[0].getnewaddress(): 1}]) - assert_raises_rpc_error(-8, 'pegin_ arguments provided but this command does not support peg-ins', self.nodes[0].walletcreatefundedpsbt, [{"txid": "0000000000000000000000000000000000000000000000000000000000000000", "vout": 0, "pegin_claim_script": "00"}], [{self.nodes[0].getnewaddress(): 1}]) - # Tests added in the 0.18 rebase don't pass on Elements yet. """ From 00eb6c0978c8cb42b5da68381d8871159fe9b5af Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 23 Oct 2019 14:12:48 -0400 Subject: [PATCH 7/7] Test funding peg-in psbt --- test/functional/feature_fedpeg.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/functional/feature_fedpeg.py b/test/functional/feature_fedpeg.py index f32edc10e61..d73fb9218f8 100755 --- a/test/functional/feature_fedpeg.py +++ b/test/functional/feature_fedpeg.py @@ -14,7 +14,8 @@ assert_equal, bytes_to_hex_str, hex_str_to_bytes, - find_vout_for_address + find_vout_for_address, + assert_greater_than ) from test_framework import util from test_framework.messages import ( @@ -317,6 +318,19 @@ def run_test(self): fin_psbt = sidechain.finalizepsbt(signed_psbt['psbt']) assert_equal(fin_psbt, signed_pegin) + # Try funding a psbt with the peg-in + assert_equal(sidechain.getbalance()['bitcoin'], 50) + out_bal = 0 + outputs.append({sidechain.getnewaddress(): 49.999}) + for out in outputs: + for val in out.values(): + out_bal += Decimal(val) + assert_greater_than(out_bal, 50) + pegin_psbt = sidechain.walletcreatefundedpsbt([{"txid":txid1, "vout": vout, "pegin_bitcoin_tx": raw, "pegin_txout_proof": proof, "pegin_claim_script": addrs["claim_script"]}], outputs) + signed_psbt = sidechain.walletsignpsbt(pegin_psbt['psbt']) + fin_psbt = sidechain.finalizepsbt(signed_psbt['psbt']) + assert fin_psbt['complete'] + sample_pegin_struct = FromHex(CTransaction(), signed_pegin["hex"]) # Round-trip peg-in transaction using python serialization assert_equal(signed_pegin["hex"], bytes_to_hex_str(sample_pegin_struct.serialize()))