mirror of
https://github.com/ElementsProject/elements.git
synced 2026-08-13 12:33:42 +02:00
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:
commit
7d759da52a
8 changed files with 399 additions and 30 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue