Merge b94d0c7af1 into merged_master (Bitcoin PR bitcoin/bitcoin#23201)

This merge was a bit tricky, since in Elements the witnesses are not
part of the transaction inputs themselves. This merge should be reviewed
carefully.
This commit is contained in:
Byron Hambly 2023-06-27 09:39:04 +00:00
commit 7d759da52a
8 changed files with 399 additions and 30 deletions

View file

@ -118,9 +118,28 @@ public:
vOutpoints.assign(setSelected.begin(), setSelected.end());
}
void SetInputWeight(const COutPoint& outpoint, int64_t weight)
{
m_input_weights[outpoint] = weight;
}
bool HasInputWeight(const COutPoint& outpoint) const
{
return m_input_weights.count(outpoint) > 0;
}
int64_t GetInputWeight(const COutPoint& outpoint) const
{
auto it = m_input_weights.find(outpoint);
assert(it != m_input_weights.end());
return it->second;
}
private:
std::set<COutPoint> setSelected;
std::map<COutPoint, CTxOut> m_external_txouts;
//! Map of COutPoints to the maximum weight for that input
std::map<COutPoint, int64_t> m_input_weights;
};
} // namespace wallet

View file

@ -3,6 +3,7 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <assetsdir.h>
#include <consensus/validation.h>
#include <core_io.h>
#include <issuance.h>
#include <key_io.h>
@ -484,6 +485,7 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out,
{"conf_target", UniValueType(UniValue::VNUM)},
{"estimate_mode", UniValueType(UniValue::VSTR)},
{"include_explicit", UniValueType(UniValue::VBOOL)},
{"input_weights", UniValueType(UniValue::VARR)},
},
true, true);
@ -628,6 +630,40 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out,
}
}
if (options.exists("input_weights")) {
for (const UniValue& input : options["input_weights"].get_array().getValues()) {
uint256 txid = ParseHashO(input, "txid");
const UniValue& vout_v = find_value(input, "vout");
if (!vout_v.isNum()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, missing vout key");
}
int vout = vout_v.get_int();
if (vout < 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout cannot be negative");
}
const UniValue& weight_v = find_value(input, "weight");
if (!weight_v.isNum()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, missing weight key");
}
int64_t weight = weight_v.get_int64();
CMutableTransaction mtx;
mtx.vin.resize(1);
mtx.witness.vtxinwit.resize(1);
const int64_t min_input_weight = GetTransactionInputWeight(CTransaction(mtx), 0);
CHECK_NONFATAL(min_input_weight == 165);
if (weight < min_input_weight) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, weight cannot be less than 165 (41 bytes (size of outpoint + sequence + empty scriptSig) * 4 (witness scaling factor)) + 1 (empty witness)");
}
if (weight > MAX_STANDARD_TX_WEIGHT) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, weight cannot be greater than the maximum standard tx weight of %d", MAX_STANDARD_TX_WEIGHT));
}
coinControl.SetInputWeight(COutPoint(txid, vout), weight);
}
}
if (tx.vout.size() == 0)
throw JSONRPCError(RPC_INVALID_PARAMETER, "TX must have at least one output");
@ -677,6 +713,23 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out,
}
}
static void SetOptionsInputWeights(const UniValue& inputs, UniValue& options)
{
if (options.exists("input_weights")) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Input weights should be specified in inputs rather than in options.");
}
if (inputs.size() == 0) {
return;
}
UniValue weights(UniValue::VARR);
for (const UniValue& input : inputs.getValues()) {
if (input.exists("weight")) {
weights.push_back(input);
}
}
options.pushKV("input_weights", weights);
}
RPCHelpMan fundrawtransaction()
{
return RPCHelpMan{"fundrawtransaction",
@ -718,6 +771,17 @@ RPCHelpMan fundrawtransaction()
{"vout_index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "The zero-based output index, before a change output is added."},
},
},
{"input_weights", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, "Inputs and their corresponding weights",
{
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"},
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output index"},
{"weight", RPCArg::Type::NUM, RPCArg::Optional::NO, "The maximum weight for this input, "
"including the weight of the outpoint and sequence number. "
"Note that serialized signature sizes are not guaranteed to be consistent, "
"so the maximum DER signatures size of 73 bytes should be used when considering ECDSA signatures."
"Remember to convert serialized sizes to weight units when necessary."},
},
},
},
FundTxDoc()),
"options"},
@ -1121,6 +1185,11 @@ RPCHelpMan send()
{"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"},
{"weight", RPCArg::Type::NUM, RPCArg::DefaultHint{"Calculated from wallet and solving data"}, "The maximum weight for this input, "
"including the weight of the outpoint and sequence number. "
"Note that signature sizes are not guaranteed to be consistent, "
"so the maximum DER signatures size of 73 bytes should be used when considering ECDSA signatures."
"Remember to convert serialized sizes to weight units when necessary."},
},
},
{"locktime", RPCArg::Type::NUM, RPCArg::Default{0}, "Raw locktime. Non-0 value also locktime-activates inputs"},
@ -1228,6 +1297,7 @@ RPCHelpMan send()
if (options.exists("solving_data")) {
solving_data = options["solving_data"].get_obj();
}
SetOptionsInputWeights(options["inputs"], options);
FundTransaction(*pwallet, rawTx, fee, change_position, options, coin_control, /* solving_data */ solving_data, /* override_min_fee */ false);
bool add_to_wallet = true;
@ -1405,6 +1475,11 @@ RPCHelpMan walletcreatefundedpsbt()
{"asset_entropy", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "For new asset issuance, this is any additional entropy to be used in the asset tag calculation. For reissuance, this is the original asaset entropy"},
{"asset_blinding_nonce", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "Do not set for new asset issuance. For reissuance, this is the blinding factor for reissuance token output for the asset being reissued"},
{"blind_reissuance", RPCArg::Type::BOOL, RPCArg::Default{true}, "Whether to mark the issuance input for blinding or not. Only affects issuances with re-issuance tokens."},
{"weight", RPCArg::Type::NUM, RPCArg::DefaultHint{"Calculated from wallet and solving data"}, "The maximum weight for this input, "
"including the weight of the outpoint and sequence number. "
"Note that signature sizes are not guaranteed to be consistent, "
"so the maximum DER signatures size of 73 bytes should be used when considering ECDSA signatures."
"Remember to convert serialized sizes to weight units when necessary."},
},
},
},
@ -1512,10 +1587,12 @@ RPCHelpMan walletcreatefundedpsbt()
}, true
);
UniValue options = request.params[3];
CAmount fee;
int change_position;
bool rbf{wallet.m_signal_rbf};
const UniValue &replaceable_arg = request.params[3]["replaceable"];
const UniValue &replaceable_arg = options["replaceable"];
if (!replaceable_arg.isNull()) {
RPCTypeCheckArgument(replaceable_arg, UniValue::VBOOL);
rbf = replaceable_arg.isTrue();
@ -1577,7 +1654,8 @@ RPCHelpMan walletcreatefundedpsbt()
txout.nNonce.vchCommitment = std::vector<unsigned char>(blind_pub.begin(), blind_pub.end());
}
}
FundTransaction(wallet, rawTx, fee, change_position, request.params[3], coin_control, /* solving_data */ request.params[5], /* override_min_fee */ true);
SetOptionsInputWeights(request.params[0], options);
FundTransaction(wallet, rawTx, fee, change_position, options, coin_control, /* solving_data */ request.params[5], /* override_min_fee */ true);
// Find an input that is ours
unsigned int blinder_index = 0;
{

View file

@ -39,13 +39,13 @@ std::string COutput::ToString(const CWallet& wallet) 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
static bool DummySignInput(const SigningProvider* provider, CMutableTransaction& tx, const size_t nIn, const CTxOut& txout, bool use_max_sig)
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(*provider, 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);
@ -59,11 +59,21 @@ bool CWallet::DummySignTx(CMutableTransaction &txNew, const std::vector<CTxOut>
int nIn = 0;
for (const auto& txout : txouts)
{
std::unique_ptr<SigningProvider> provider = GetSolvingProvider(txout.scriptPubKey);
CTxIn& txin = txNew.vin[nIn];
// If weight was provided, fill the input to that weight
if (coin_control && coin_control->HasInputWeight(txin.prevout)) {
if (!FillInputToWeight(txNew, nIn, coin_control->GetInputWeight(txin.prevout))) {
return false;
}
nIn++;
continue;
}
// 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 (!provider || !DummySignInput(provider.get(), txNew, nIn, txout, use_max_sig)) {
if (!coin_control || !DummySignInput(&coin_control->m_external_provider, txNew, nIn, txout, use_max_sig)) {
// 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, txNew, nIn, txout, use_max_sig)) {
if (!coin_control || !DummySignInput(coin_control->m_external_provider, txNew, nIn, txout, use_max_sig)) {
return false;
}
}
@ -73,10 +83,53 @@ bool CWallet::DummySignTx(CMutableTransaction &txNew, const std::vector<CTxOut>
return true;
}
bool FillInputToWeight(CMutableTransaction& mtx, size_t nIn, int64_t target_weight)
{
assert(mtx.vin[nIn].scriptSig.empty());
assert(mtx.witness.vtxinwit[nIn].scriptWitness.IsNull());
int64_t txin_weight = GetTransactionInputWeight(CTransaction(mtx), nIn);
// Do nothing if the weight that should be added is less than the weight that already exists
if (target_weight < txin_weight) {
return false;
}
if (target_weight == txin_weight) {
return true;
}
// Subtract current txin weight, which should include empty witness stack
int64_t add_weight = target_weight - txin_weight;
assert(add_weight > 0);
// We will want to subtract the size of the Compact Size UInt that will also be serialized.
// However doing so when the size is near a boundary can result in a problem where it is not
// possible to have a stack element size and combination to exactly equal a target.
// To avoid this possibility, if the weight to add is less than 10 bytes greater than
// a boundary, the size will be split so that 2/3rds will be in one stack element, and
// the remaining 1/3rd in another. Using 3rds allows us to avoid additional boundaries.
// 10 bytes is used because that accounts for the maximum size. This does not need to be super precise.
if ((add_weight >= 253 && add_weight < 263)
|| (add_weight > std::numeric_limits<uint16_t>::max() && add_weight <= std::numeric_limits<uint16_t>::max() + 10)
|| (add_weight > std::numeric_limits<uint32_t>::max() && add_weight <= std::numeric_limits<uint32_t>::max() + 10)) {
int64_t first_weight = add_weight / 3;
add_weight -= first_weight;
first_weight -= GetSizeOfCompactSize(first_weight);
mtx.witness.vtxinwit[nIn].scriptWitness.stack.emplace(mtx.witness.vtxinwit[nIn].scriptWitness.stack.end(), first_weight, 0);
}
add_weight -= GetSizeOfCompactSize(add_weight);
mtx.witness.vtxinwit[nIn].scriptWitness.stack.emplace(mtx.witness.vtxinwit[nIn].scriptWitness.stack.end(), add_weight, 0);
assert(GetTransactionInputWeight(CTransaction(mtx), nIn) == target_weight);
return true;
}
int CalculateMaximumSignedInputSize(const CTxOut& txout, const SigningProvider* provider, bool use_max_sig) {
CMutableTransaction txn;
txn.vin.push_back(CTxIn(COutPoint()));
if (!provider || !DummySignInput(provider, txn, 0, txout, use_max_sig)) {
if (!provider || !DummySignInput(*provider, txn, 0, txout, use_max_sig)) {
return -1;
}
return GetVirtualTransactionInputSize(CTransaction(txn));
@ -544,13 +597,11 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec
continue;
}
input_bytes = GetTxSpendSize(wallet, wtx, outpoint.n, false);
txout = wtx.tx->vout[outpoint.n];
txout = wtx.tx->vout.at(outpoint.n);
coin = CInputCoin(wallet, &wtx, outpoint.n, input_bytes);
}
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
} else {
// The input is external. We did not find the tx in mapWallet.
if (!coin_control.GetExternalOutput(outpoint, txout)) {
// Not ours, and we don't have solving data.
return std::nullopt;
}
input_bytes = CalculateMaximumSignedInputSize(txout, &coin_control.m_external_provider, /* use_max_sig */ true);
@ -568,6 +619,11 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec
}
coin = CInputCoin(outpoint, txout, input_bytes);
}
// If available, override calculated size with coin control specified size
if (coin_control.HasInputWeight(outpoint)) {
input_bytes = GetVirtualTransactionSize(coin_control.GetInputWeight(outpoint), 0, 0);
coin = CInputCoin(outpoint, txout, input_bytes);
}
mapValueFromPresetInputs[coin.asset] += coin.value;
if (coin.m_input_bytes == -1) {

View file

@ -63,5 +63,64 @@ BOOST_FIXTURE_TEST_CASE(SubtractFee, TestChain100Setup)
BOOST_CHECK_EQUAL(fee, check_tx(fee + 123));
}
static void TestFillInputToWeight(int64_t additional_weight, std::vector<int64_t> expected_stack_sizes)
{
CMutableTransaction mtx;
mtx.vin.resize(1);
mtx.witness.vtxinwit.resize(1);
static const int64_t EMPTY_INPUT_WEIGHT = GetTransactionInputWeight(CTransaction(mtx), 0);
CTxIn input;
int64_t target_weight = EMPTY_INPUT_WEIGHT + additional_weight;
size_t nIn{0};
BOOST_CHECK(FillInputToWeight(mtx, nIn, target_weight));
BOOST_CHECK_EQUAL(GetTransactionInputWeight(CTransaction(mtx), nIn), target_weight);
BOOST_CHECK_EQUAL(mtx.witness.vtxinwit[nIn].scriptWitness.stack.size(), expected_stack_sizes.size());
for (unsigned int i = 0; i < expected_stack_sizes.size(); ++i) {
BOOST_CHECK_EQUAL(mtx.witness.vtxinwit[nIn].scriptWitness.stack[i].size(), expected_stack_sizes[i]);
}
}
BOOST_FIXTURE_TEST_CASE(FillInputToWeightTest, BasicTestingSetup)
{
{
// Less than or equal minimum of 165 should not add any witness data
CTxIn input;
CMutableTransaction mtx;
mtx.vin.resize(1);
mtx.witness.vtxinwit.resize(1);
size_t nIn{0};
BOOST_CHECK(!FillInputToWeight(mtx, nIn, -1));
BOOST_CHECK_EQUAL(GetTransactionInputWeight(CTransaction(mtx), nIn), 165);
BOOST_CHECK_EQUAL(mtx.witness.vtxinwit[nIn].scriptWitness.stack.size(), 0);
BOOST_CHECK(!FillInputToWeight(mtx, nIn, 0));
BOOST_CHECK_EQUAL(GetTransactionInputWeight(CTransaction(mtx), nIn), 165);
BOOST_CHECK_EQUAL(mtx.witness.vtxinwit[nIn].scriptWitness.stack.size(), 0);
BOOST_CHECK(!FillInputToWeight(mtx, nIn, 164));
BOOST_CHECK_EQUAL(GetTransactionInputWeight(CTransaction(mtx), nIn), 165);
BOOST_CHECK_EQUAL(mtx.witness.vtxinwit[nIn].scriptWitness.stack.size(), 0);
BOOST_CHECK(FillInputToWeight(mtx, nIn, 165));
BOOST_CHECK_EQUAL(GetTransactionInputWeight(CTransaction(mtx), nIn), 165);
BOOST_CHECK_EQUAL(mtx.witness.vtxinwit[nIn].scriptWitness.stack.size(), 0);
}
// Make sure we can add at least one weight
TestFillInputToWeight(1, {0});
// 1 byte compact size uint boundary
TestFillInputToWeight(252, {251});
TestFillInputToWeight(253, {83, 168});
TestFillInputToWeight(262, {86, 174});
TestFillInputToWeight(263, {260});
// 3 byte compact size uint boundary
TestFillInputToWeight(65535, {65532});
TestFillInputToWeight(65536, {21842, 43688});
TestFillInputToWeight(65545, {21845, 43694});
TestFillInputToWeight(65546, {65541});
// Note: We don't test the next boundary because of memory allocation constraints.
}
BOOST_AUTO_TEST_SUITE_END()
} // namespace wallet

View file

@ -1001,7 +1001,9 @@ bool AddWalletSetting(interfaces::Chain& chain, const std::string& wallet_name);
//! 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 DummySignInput(const SigningProvider& provider, CTxIn &tx_in, const CTxOut &txout, bool use_max_sig);
bool DummySignInput(const SigningProvider& provider, CMutableTransaction& tx, const size_t nIn, const CTxOut& txout, bool use_max_sig);
bool FillInputToWeight(CMutableTransaction& mtx, size_t nIn, int64_t target_weight);
} // namespace wallet
#endif // BITCOIN_WALLET_WALLET_H

View file

@ -4,8 +4,10 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test the fundrawtransaction RPC."""
from decimal import Decimal
from itertools import product
from math import ceil
from test_framework.descriptors import descsum_create
from test_framework.key import ECKey
@ -625,7 +627,6 @@ class RawTransactionsTest(BitcoinTestFramework):
value = 1.09997500
outputs = [{self.nodes[0].getnewaddress():value}]
rawtx = self.nodes[1].createrawtransaction(inputs, outputs)
print(self.nodes[1].decoderawtransaction(rawtx))
# fund a transaction that does not require a new key for the change output
self.nodes[1].fundrawtransaction(rawtx)
@ -1080,17 +1081,23 @@ class RawTransactionsTest(BitcoinTestFramework):
ext_utxo = ext_wallet.listunspent(addresses=[addr])[0]
# An external input without solving data should result in an error
raw_tx = ext_wallet.createrawtransaction([ext_utxo], [{ext_fund.getnewaddress(): 15}])
raw_tx = ext_wallet.createrawtransaction([ext_utxo], [{ext_wallet.getnewaddress(): ext_utxo["amount"] / 2}])
# ELEMENTS
# This bitcoin assert is no longer valid because we had to generate a bunch of blocks
# above to fund ext_wallet
#assert_raises_rpc_error(-4, "Insufficient funds", ext_fund.fundrawtransaction, raw_tx)
#assert_raises_rpc_error(-4, "Insufficient funds", wallet.fundrawtransaction, raw_tx)
# Error conditions
assert_raises_rpc_error(-5, "'not a pubkey' is not hex", ext_fund.fundrawtransaction, raw_tx, {"solving_data": {"pubkeys":["not a pubkey"]}})
assert_raises_rpc_error(-5, "'01234567890a0b0c0d0e0f' is not a valid public key", ext_fund.fundrawtransaction, raw_tx, {"solving_data": {"pubkeys":["01234567890a0b0c0d0e0f"]}})
assert_raises_rpc_error(-5, "'not a script' is not hex", ext_fund.fundrawtransaction, raw_tx, {"solving_data": {"scripts":["not a script"]}})
assert_raises_rpc_error(-8, "Unable to parse descriptor 'not a descriptor'", ext_fund.fundrawtransaction, raw_tx, {"solving_data": {"descriptors":["not a descriptor"]}})
assert_raises_rpc_error(-8, "Invalid parameter, missing vout key", ext_fund.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"]}]})
assert_raises_rpc_error(-8, "Invalid parameter, vout cannot be negative", ext_fund.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": -1}]})
assert_raises_rpc_error(-8, "Invalid parameter, missing weight key", ext_fund.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"]}]})
assert_raises_rpc_error(-8, "Invalid parameter, weight cannot be less than 165", ext_fund.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 164}]})
assert_raises_rpc_error(-8, "Invalid parameter, weight cannot be less than 165", ext_fund.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": -1}]})
assert_raises_rpc_error(-8, "Invalid parameter, weight cannot be greater than", ext_fund.fundrawtransaction, raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 400001}]})
# But funding should work when the solving data is provided
funded_tx = ext_fund.fundrawtransaction(raw_tx, {"solving_data": {"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]}})
@ -1100,10 +1107,44 @@ class RawTransactionsTest(BitcoinTestFramework):
assert signed_tx['complete']
funded_tx = ext_fund.fundrawtransaction(raw_tx, {"solving_data": {"descriptors": [desc]}})
signed_tx = ext_fund.signrawtransactionwithwallet(funded_tx['hex'])
assert not signed_tx['complete']
signed_tx = ext_wallet.signrawtransactionwithwallet(signed_tx['hex'])
assert signed_tx['complete']
signed_tx1 = ext_fund.signrawtransactionwithwallet(funded_tx['hex'])
assert not signed_tx1['complete']
signed_tx2 = ext_wallet.signrawtransactionwithwallet(signed_tx1['hex'])
assert signed_tx2['complete']
unsigned_weight = ext_wallet.decoderawtransaction(signed_tx1["hex"])["weight"]
signed_weight = ext_wallet.decoderawtransaction(signed_tx2["hex"])["weight"]
# Input's weight is difference between weight of signed and unsigned,
# and the weight of stuff that didn't change (prevout, sequence, 1 byte of scriptSig)
input_weight = signed_weight - unsigned_weight + (41 * 4)
low_input_weight = input_weight // 2
high_input_weight = input_weight * 2
# Funding should also work if the input weight is provided
funded_tx = ext_fund.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": input_weight}]})
signed_tx = ext_fund.signrawtransactionwithwallet(funded_tx["hex"])
signed_tx = ext_wallet.signrawtransactionwithwallet(signed_tx["hex"])
assert_equal(ext_wallet.testmempoolaccept([signed_tx["hex"]])[0]["allowed"], True)
assert_equal(signed_tx["complete"], True)
# Reducing the weight should have a lower fee
funded_tx2 = ext_fund.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": low_input_weight}]})
assert_greater_than(funded_tx["fee"], funded_tx2["fee"])
# Increasing the weight should have a higher fee
funded_tx2 = ext_fund.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}]})
assert_greater_than(funded_tx2["fee"], funded_tx["fee"])
# The provided weight should override the calculated weight when solving data is provided
funded_tx3 = ext_fund.fundrawtransaction(raw_tx, {"solving_data": {"descriptors": [desc]}, "input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}]})
assert_equal(funded_tx2["fee"], funded_tx3["fee"])
# The feerate should be met
funded_tx4 = ext_fund.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}], "fee_rate": 10})
input_add_weight = high_input_weight - (41 * 4)
tx4_weight = ext_fund.decoderawtransaction(funded_tx4["hex"])["weight"] + input_add_weight
tx4_vsize = int(ceil(tx4_weight / 4))
# assert_fee_amount(funded_tx4["fee"], tx4_vsize, Decimal(0.0001))
# Funding with weight at csuint boundaries should not cause problems
funded_tx = ext_fund.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 255}]})
funded_tx = ext_fund.fundrawtransaction(raw_tx, {"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 65539}]})
self.nodes[2].unloadwallet("extfund")
self.nodes[0].unloadwallet("extsend")

View file

@ -1114,11 +1114,15 @@ class PSBTTest(BitcoinTestFramework):
self.log.info("Try decoding and combining transactions in various states of blindedness")
self.pset_confidential_proofs()
# Test that we can fund psbts with external inputs specified
self.log.info("Test that we can fund psbts with external inputs specified")
eckey = ECKey()
eckey.generate()
privkey = bytes_to_wif(eckey.get_bytes())
self.nodes[1].createwallet("extfund")
wallet = 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:
@ -1130,27 +1134,97 @@ class PSBTTest(BitcoinTestFramework):
addr_info = self.nodes[0].getaddressinfo(addr)
self.nodes[0].sendtoaddress(addr, 10)
self.nodes[0].sendtoaddress(wallet.getnewaddress(), 10)
self.generate(self.nodes[0], 6)
ext_utxo = self.nodes[0].listunspent(addresses=[addr])[0]
# An external input without solving data should result in an error
# ELEMENTS: minor FIXME error is different since SelectCoins no longer has the error out
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})
assert_raises_rpc_error(-4, "Insufficient funds", wallet.walletcreatefundedpsbt, [ext_utxo], [{self.nodes[0].getnewaddress(): 15}])
# 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'])
psbt = wallet.walletcreatefundedpsbt([ext_utxo], [{self.nodes[0].getnewaddress(): 15}], 0, {"add_inputs": True, "solving_data": {"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]}})
signed = wallet.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'])
psbt = wallet.walletcreatefundedpsbt([ext_utxo], [{self.nodes[0].getnewaddress(): 15}], 0, {"add_inputs": True, "solving_data":{"descriptors": [desc]}})
signed = wallet.walletprocesspsbt(psbt['psbt'])
assert not signed['complete']
signed = self.nodes[0].walletprocesspsbt(signed['psbt'])
assert signed['complete']
self.nodes[0].finalizepsbt(signed['psbt'])
final = self.nodes[0].finalizepsbt(signed['psbt'], False)
dec = self.nodes[0].decodepsbt(signed["psbt"])
for i, txin in enumerate(dec["inputs"]):
if txin["previous_txid"] == ext_utxo["txid"] and txin["previous_vout"] == ext_utxo["vout"]:
input_idx = i
break
psbt_in = dec["inputs"][input_idx]
# Calculate the input weight
# (prevout + sequence + length of scriptSig + 2 bytes buffer) * 4 + len of scriptwitness
len_scriptsig = len(psbt_in["final_scriptSig"]["hex"]) // 2 if "final_scriptSig" in psbt_in else 0
len_scriptwitness = len(psbt_in["final_scriptwitness"]["hex"]) // 2 if "final_scriptwitness" in psbt_in else 0
input_weight = ((41 + len_scriptsig + 2) * 4) + len_scriptwitness
low_input_weight = input_weight // 2
high_input_weight = input_weight * 2
# Input weight error conditions
assert_raises_rpc_error(
-8,
"Input weights should be specified in inputs rather than in options.",
wallet.walletcreatefundedpsbt,
inputs=[ext_utxo],
outputs=[{self.nodes[0].getnewaddress(): 15}],
options={"input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 1000}]}
)
# Funding should also work if the input weight is provided
psbt = wallet.walletcreatefundedpsbt(
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": input_weight}],
outputs=[{self.nodes[0].getnewaddress(): 15}],
options={"add_inputs": True}
)
signed = wallet.walletprocesspsbt(psbt["psbt"])
signed = self.nodes[0].walletprocesspsbt(signed["psbt"])
final = self.nodes[0].finalizepsbt(signed["psbt"])
assert self.nodes[0].testmempoolaccept([final["hex"]])[0]["allowed"]
# Reducing the weight should have a lower fee
psbt2 = wallet.walletcreatefundedpsbt(
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": low_input_weight}],
outputs=[{self.nodes[0].getnewaddress(): 15}],
options={"add_inputs": True}
)
assert_greater_than(psbt["fee"], psbt2["fee"])
# Increasing the weight should have a higher fee
psbt2 = wallet.walletcreatefundedpsbt(
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}],
outputs=[{self.nodes[0].getnewaddress(): 15}],
options={"add_inputs": True}
)
assert_greater_than(psbt2["fee"], psbt["fee"])
# The provided weight should override the calculated weight when solving data is provided
psbt3 = wallet.walletcreatefundedpsbt(
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}],
outputs=[{self.nodes[0].getnewaddress(): 15}],
options={'add_inputs': True, "solving_data":{"descriptors": [desc]}}
)
assert_equal(psbt2["fee"], psbt3["fee"])
# Import the external utxo descriptor so that we can sign for it from the test wallet
if self.options.descriptors:
res = wallet.importdescriptors([{"desc": desc, "timestamp": "now"}])
else:
res = wallet.importmulti([{"desc": desc, "timestamp": "now"}])
assert res[0]["success"]
# The provided weight should override the calculated weight for a wallet input
psbt3 = wallet.walletcreatefundedpsbt(
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": high_input_weight}],
outputs=[{self.nodes[0].getnewaddress(): 15}],
options={"add_inputs": True}
)
assert_equal(psbt2["fee"], psbt3["fee"])
if __name__ == '__main__':
PSBTTest().main()

View file

@ -524,5 +524,45 @@ class WalletSendTest(BitcoinTestFramework):
assert signed["complete"]
self.nodes[0].finalizepsbt(signed["psbt"])
dec = self.nodes[0].decodepsbt(signed["psbt"])
for i, txin in enumerate(dec["inputs"]):
if txin["previous_txid"] == ext_utxo["txid"] and txin["previous_vout"] == ext_utxo["vout"]:
input_idx = i
break
psbt_in = dec["inputs"][input_idx]
# Calculate the input weight
# (prevout + sequence + length of scriptSig + 2 bytes buffer) * 4 + len of scriptwitness
len_scriptsig = len(psbt_in["final_scriptSig"]["hex"]) // 2 if "final_scriptSig" in psbt_in else 0
len_scriptwitness = len(psbt_in["final_scriptwitness"]["hex"]) // 2 if "final_scriptwitness" in psbt_in else 0
input_weight = ((41 + len_scriptsig + 2) * 4) + len_scriptwitness
# Input weight error conditions
assert_raises_rpc_error(
-8,
"Input weights should be specified in inputs rather than in options.",
ext_wallet.send,
outputs=[{self.nodes[0].getnewaddress(): 15}],
options={"inputs": [ext_utxo], "input_weights": [{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 1000}]}
)
# Funding should also work when input weights are provided
res = self.test_send(
from_wallet=ext_wallet,
to_wallet=self.nodes[0],
amount=15,
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": input_weight}],
add_inputs=True,
psbt=True,
include_watching=True,
fee_rate=10
)
signed = ext_wallet.walletprocesspsbt(res["psbt"])
signed = ext_fund.walletprocesspsbt(res["psbt"])
assert signed["complete"]
tx = self.nodes[0].finalizepsbt(signed["psbt"])
testres = self.nodes[0].testmempoolaccept([tx["hex"]])[0]
assert_equal(testres["allowed"], True)
# assert_fee_amount(testres["fees"]["base"], testres["vsize"], Decimal(0.0001))
if __name__ == '__main__':
WalletSendTest().main()