Merge bitcoin/bitcoin#32896: wallet, rpc: add v3 transaction creation and wallet support
5c8bf7b39edoc: add release notes for version 3 transactions (ishaanam)4ef8065a5etest: add truc wallet tests (ishaanam)5d932e14dbtest: extract `bulk_vout` from `bulk_tx` so it can be used by wallet tests (ishaanam)2cb473d9f2rpc: Support version 3 transaction creation (Bue-von-hon)4c20343b4drpc: Add transaction min standard version parameter (Bue-von-hon)c5a2d08011wallet: don't return utxos from multiple truc txs in AvailableCoins (ishaanam)da8748ad62wallet: limit v3 tx weight in coin selection (ishaanam)85c5410615wallet: mark unconfirmed v3 siblings as mempool conflicts (ishaanam)0804fc3cb1wallet: throw error at conflicting tx versions in pre-selected inputs (ishaanam)cc155226fewallet: set m_version in coin control to default value (ishaanam)2e9617664ewallet: don't include unconfirmed v3 txs with children in available coins (ishaanam)ec2676becdwallet: unconfirmed ancestors and descendants are always truc (ishaanam) Pull request description: This PR Implements the following: - If creating a v3 transaction, `AvailableCoins` doesn't return unconfirmed v2 utxos (and vice versa) - `AvailableCoins` doesn't return an unconfirmed v3 utxo if its transaction already has a child - If a v3 transaction is kicked out of the mempool by a sibling, mark the sibling as a mempool conflict - Throw an error if pre-selected inputs are of the wrong transaction version - Allow setting version to 3 manually in `createrawtransaction` (uses commits from #31936) - Limits a v3 transaction weight in coin selection Closes #31348 To-Do: - [x] Test a v3 sibling conflict kicking out one of our transactions from the mempool - [x] Implement separate size limit for TRUC children - [x] Test that we can't fund a v2 transaction when everything is v3 unconfirmed - [x] Test a v3 sibling conflict being removed from the mempool - [x] Test limiting v3 transaction weight in coin selection - [x] Simplify tests - [x] Add documentation - [x] Test that user-input max weight is not overwritten by truc max weight - [x] Test v3 in RPCs other than `createrawtransaction` ACKs for top commit: glozow: reACK5c8bf7b39eachow101: ACK5c8bf7b39erkrux: ACK5c8bf7b39eTree-SHA512: da8aea51c113e193dd0b442eff765bd6b8dc0e5066272d3e52190a223c903f48788795f32c554f268af0d2607b5b8c3985c648879cb176c65540837c05d0abb5
This commit is contained in:
25 files changed
+836
-32
No files matched your search
@@ -0,0 +1,19 @@
|
|||||||
|
Updated RPCs
|
||||||
|
------------
|
||||||
|
The following RPCs now contain a `version` parameter that allows
|
||||||
|
the user to create transactions of any standard version number (1-3):
|
||||||
|
- `createrawtransaction`
|
||||||
|
- `createpsbt`
|
||||||
|
- `send`
|
||||||
|
- `sendall`
|
||||||
|
- `walletcreatefundedpsbt`
|
||||||
|
|
||||||
|
Wallet
|
||||||
|
------
|
||||||
|
Support has been added for spending TRUC transactions received by the
|
||||||
|
wallet, as well as creating TRUC transactions. The wallet ensures that
|
||||||
|
TRUC policy rules are being met. The wallet will throw an error if the
|
||||||
|
user is trying to spend TRUC utxos with utxos of other versions.
|
||||||
|
Additionally, the wallet will treat unconfirmed TRUC sibling
|
||||||
|
transactions as mempool conflicts. The wallet will also ensure that
|
||||||
|
transactions spending TRUC utxos meet the required size restrictions.
|
||||||
@@ -98,7 +98,7 @@ bool IsStandard(const CScript& scriptPubKey, TxoutType& whichType)
|
|||||||
|
|
||||||
bool IsStandardTx(const CTransaction& tx, const std::optional<unsigned>& max_datacarrier_bytes, bool permit_bare_multisig, const CFeeRate& dust_relay_fee, std::string& reason)
|
bool IsStandardTx(const CTransaction& tx, const std::optional<unsigned>& max_datacarrier_bytes, bool permit_bare_multisig, const CFeeRate& dust_relay_fee, std::string& reason)
|
||||||
{
|
{
|
||||||
if (tx.version > TX_MAX_STANDARD_VERSION || tx.version < 1) {
|
if (tx.version > TX_MAX_STANDARD_VERSION || tx.version < TX_MIN_STANDARD_VERSION) {
|
||||||
reason = "version";
|
reason = "version";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ std::vector<uint32_t> GetDust(const CTransaction& tx, CFeeRate dust_relay_rate);
|
|||||||
// Changing the default transaction version requires a two step process: first
|
// Changing the default transaction version requires a two step process: first
|
||||||
// adapting relay policy by bumping TX_MAX_STANDARD_VERSION, and then later
|
// adapting relay policy by bumping TX_MAX_STANDARD_VERSION, and then later
|
||||||
// allowing the new transaction version in the wallet/RPC.
|
// allowing the new transaction version in the wallet/RPC.
|
||||||
|
static constexpr decltype(CTransaction::version) TX_MIN_STANDARD_VERSION{1};
|
||||||
static constexpr decltype(CTransaction::version) TX_MAX_STANDARD_VERSION{3};
|
static constexpr decltype(CTransaction::version) TX_MAX_STANDARD_VERSION{3};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ static constexpr unsigned int TRUC_ANCESTOR_LIMIT{2};
|
|||||||
|
|
||||||
/** Maximum sigop-adjusted virtual size of all v3 transactions. */
|
/** Maximum sigop-adjusted virtual size of all v3 transactions. */
|
||||||
static constexpr int64_t TRUC_MAX_VSIZE{10000};
|
static constexpr int64_t TRUC_MAX_VSIZE{10000};
|
||||||
|
static constexpr int64_t TRUC_MAX_WEIGHT{TRUC_MAX_VSIZE * WITNESS_SCALE_FACTOR};
|
||||||
/** Maximum sigop-adjusted virtual size of a tx which spends from an unconfirmed TRUC transaction. */
|
/** Maximum sigop-adjusted virtual size of a tx which spends from an unconfirmed TRUC transaction. */
|
||||||
static constexpr int64_t TRUC_CHILD_MAX_VSIZE{1000};
|
static constexpr int64_t TRUC_CHILD_MAX_VSIZE{1000};
|
||||||
|
static constexpr int64_t TRUC_CHILD_MAX_WEIGHT{TRUC_CHILD_MAX_VSIZE * WITNESS_SCALE_FACTOR};
|
||||||
// These limits are within the default ancestor/descendant limits.
|
// These limits are within the default ancestor/descendant limits.
|
||||||
static_assert(TRUC_MAX_VSIZE + TRUC_CHILD_MAX_VSIZE <= DEFAULT_ANCESTOR_SIZE_LIMIT_KVB * 1000);
|
static_assert(TRUC_MAX_VSIZE + TRUC_CHILD_MAX_VSIZE <= DEFAULT_ANCESTOR_SIZE_LIMIT_KVB * 1000);
|
||||||
static_assert(TRUC_MAX_VSIZE + TRUC_CHILD_MAX_VSIZE <= DEFAULT_DESCENDANT_SIZE_LIMIT_KVB * 1000);
|
static_assert(TRUC_MAX_VSIZE + TRUC_CHILD_MAX_VSIZE <= DEFAULT_DESCENDANT_SIZE_LIMIT_KVB * 1000);
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||||||
{ "createrawtransaction", 1, "outputs" },
|
{ "createrawtransaction", 1, "outputs" },
|
||||||
{ "createrawtransaction", 2, "locktime" },
|
{ "createrawtransaction", 2, "locktime" },
|
||||||
{ "createrawtransaction", 3, "replaceable" },
|
{ "createrawtransaction", 3, "replaceable" },
|
||||||
|
{ "createrawtransaction", 4, "version" },
|
||||||
{ "decoderawtransaction", 1, "iswitness" },
|
{ "decoderawtransaction", 1, "iswitness" },
|
||||||
{ "signrawtransactionwithkey", 1, "privkeys" },
|
{ "signrawtransactionwithkey", 1, "privkeys" },
|
||||||
{ "signrawtransactionwithkey", 2, "prevtxs" },
|
{ "signrawtransactionwithkey", 2, "prevtxs" },
|
||||||
@@ -167,6 +168,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||||||
{ "walletcreatefundedpsbt", 3, "solving_data"},
|
{ "walletcreatefundedpsbt", 3, "solving_data"},
|
||||||
{ "walletcreatefundedpsbt", 3, "max_tx_weight"},
|
{ "walletcreatefundedpsbt", 3, "max_tx_weight"},
|
||||||
{ "walletcreatefundedpsbt", 4, "bip32derivs" },
|
{ "walletcreatefundedpsbt", 4, "bip32derivs" },
|
||||||
|
{ "walletcreatefundedpsbt", 5, "version" },
|
||||||
{ "walletprocesspsbt", 1, "sign" },
|
{ "walletprocesspsbt", 1, "sign" },
|
||||||
{ "walletprocesspsbt", 3, "bip32derivs" },
|
{ "walletprocesspsbt", 3, "bip32derivs" },
|
||||||
{ "walletprocesspsbt", 4, "finalize" },
|
{ "walletprocesspsbt", 4, "finalize" },
|
||||||
@@ -177,6 +179,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||||||
{ "createpsbt", 1, "outputs" },
|
{ "createpsbt", 1, "outputs" },
|
||||||
{ "createpsbt", 2, "locktime" },
|
{ "createpsbt", 2, "locktime" },
|
||||||
{ "createpsbt", 3, "replaceable" },
|
{ "createpsbt", 3, "replaceable" },
|
||||||
|
{ "createpsbt", 4, "version" },
|
||||||
{ "combinepsbt", 0, "txs"},
|
{ "combinepsbt", 0, "txs"},
|
||||||
{ "joinpsbts", 0, "txs"},
|
{ "joinpsbts", 0, "txs"},
|
||||||
{ "finalizepsbt", 1, "extract"},
|
{ "finalizepsbt", 1, "extract"},
|
||||||
@@ -213,6 +216,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||||||
{ "send", 4, "replaceable"},
|
{ "send", 4, "replaceable"},
|
||||||
{ "send", 4, "solving_data"},
|
{ "send", 4, "solving_data"},
|
||||||
{ "send", 4, "max_tx_weight"},
|
{ "send", 4, "max_tx_weight"},
|
||||||
|
{ "send", 5, "version"},
|
||||||
{ "sendall", 0, "recipients" },
|
{ "sendall", 0, "recipients" },
|
||||||
{ "sendall", 1, "conf_target" },
|
{ "sendall", 1, "conf_target" },
|
||||||
{ "sendall", 3, "fee_rate"},
|
{ "sendall", 3, "fee_rate"},
|
||||||
@@ -230,6 +234,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||||||
{ "sendall", 4, "conf_target"},
|
{ "sendall", 4, "conf_target"},
|
||||||
{ "sendall", 4, "replaceable"},
|
{ "sendall", 4, "replaceable"},
|
||||||
{ "sendall", 4, "solving_data"},
|
{ "sendall", 4, "solving_data"},
|
||||||
|
{ "sendall", 4, "version"},
|
||||||
{ "simulaterawtransaction", 0, "rawtxs" },
|
{ "simulaterawtransaction", 0, "rawtxs" },
|
||||||
{ "simulaterawtransaction", 1, "options" },
|
{ "simulaterawtransaction", 1, "options" },
|
||||||
{ "simulaterawtransaction", 1, "include_watchonly"},
|
{ "simulaterawtransaction", 1, "include_watchonly"},
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ using node::GetTransaction;
|
|||||||
using node::NodeContext;
|
using node::NodeContext;
|
||||||
using node::PSBTAnalysis;
|
using node::PSBTAnalysis;
|
||||||
|
|
||||||
|
static constexpr decltype(CTransaction::version) DEFAULT_RAWTX_VERSION{CTransaction::CURRENT_VERSION};
|
||||||
|
|
||||||
static void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry,
|
static void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry,
|
||||||
Chainstate& active_chainstate, const CTxUndo* txundo = nullptr,
|
Chainstate& active_chainstate, const CTxUndo* txundo = nullptr,
|
||||||
TxVerbosity verbosity = TxVerbosity::SHOW_DETAILS)
|
TxVerbosity verbosity = TxVerbosity::SHOW_DETAILS)
|
||||||
@@ -158,6 +160,7 @@ static std::vector<RPCArg> CreateTxDoc()
|
|||||||
{"locktime", RPCArg::Type::NUM, RPCArg::Default{0}, "Raw locktime. Non-0 value also locktime-activates inputs"},
|
{"locktime", RPCArg::Type::NUM, RPCArg::Default{0}, "Raw locktime. Non-0 value also locktime-activates inputs"},
|
||||||
{"replaceable", RPCArg::Type::BOOL, RPCArg::Default{true}, "Marks this transaction as BIP125-replaceable.\n"
|
{"replaceable", RPCArg::Type::BOOL, RPCArg::Default{true}, "Marks this transaction as BIP125-replaceable.\n"
|
||||||
"Allows this transaction to be replaced by a transaction with higher fees. If provided, it is an error if explicit sequence numbers are incompatible."},
|
"Allows this transaction to be replaced by a transaction with higher fees. If provided, it is an error if explicit sequence numbers are incompatible."},
|
||||||
|
{"version", RPCArg::Type::NUM, RPCArg::Default{DEFAULT_RAWTX_VERSION}, "Transaction version"},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +440,7 @@ static RPCHelpMan createrawtransaction()
|
|||||||
if (!request.params[3].isNull()) {
|
if (!request.params[3].isNull()) {
|
||||||
rbf = request.params[3].get_bool();
|
rbf = request.params[3].get_bool();
|
||||||
}
|
}
|
||||||
CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf);
|
CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf, self.Arg<uint32_t>("version"));
|
||||||
|
|
||||||
return EncodeHexTx(CTransaction(rawTx));
|
return EncodeHexTx(CTransaction(rawTx));
|
||||||
},
|
},
|
||||||
@@ -1679,7 +1682,7 @@ static RPCHelpMan createpsbt()
|
|||||||
if (!request.params[3].isNull()) {
|
if (!request.params[3].isNull()) {
|
||||||
rbf = request.params[3].get_bool();
|
rbf = request.params[3].get_bool();
|
||||||
}
|
}
|
||||||
CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf);
|
CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf, self.Arg<uint32_t>("version"));
|
||||||
|
|
||||||
// Make a blank psbt
|
// Make a blank psbt
|
||||||
PartiallySignedTransaction psbtx;
|
PartiallySignedTransaction psbtx;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include <tinyformat.h>
|
#include <tinyformat.h>
|
||||||
#include <univalue.h>
|
#include <univalue.h>
|
||||||
#include <util/rbf.h>
|
#include <util/rbf.h>
|
||||||
|
#include <util/string.h>
|
||||||
#include <util/strencodings.h>
|
#include <util/strencodings.h>
|
||||||
#include <util/translation.h>
|
#include <util/translation.h>
|
||||||
|
|
||||||
@@ -143,7 +144,7 @@ void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional<bool> rbf)
|
CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional<bool> rbf, const uint32_t version)
|
||||||
{
|
{
|
||||||
CMutableTransaction rawTx;
|
CMutableTransaction rawTx;
|
||||||
|
|
||||||
@@ -154,6 +155,11 @@ CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniVal
|
|||||||
rawTx.nLockTime = nLockTime;
|
rawTx.nLockTime = nLockTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version < TX_MIN_STANDARD_VERSION || version > TX_MAX_STANDARD_VERSION) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, version out of range(%d~%d)", TX_MIN_STANDARD_VERSION, TX_MAX_STANDARD_VERSION));
|
||||||
|
}
|
||||||
|
rawTx.version = version;
|
||||||
|
|
||||||
AddInputs(rawTx, inputs_in, rbf);
|
AddInputs(rawTx, inputs_in, rbf);
|
||||||
AddOutputs(rawTx, outputs_in);
|
AddOutputs(rawTx, outputs_in);
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,6 @@ std::vector<std::pair<CTxDestination, CAmount>> ParseOutputs(const UniValue& out
|
|||||||
void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in);
|
void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in);
|
||||||
|
|
||||||
/** Create a transaction from univalue parameters */
|
/** Create a transaction from univalue parameters */
|
||||||
CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional<bool> rbf);
|
CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional<bool> rbf, const uint32_t version);
|
||||||
|
|
||||||
#endif // BITCOIN_RPC_RAWTRANSACTION_UTIL_H
|
#endif // BITCOIN_RPC_RAWTRANSACTION_UTIL_H
|
||||||
@@ -731,6 +731,7 @@ TMPL_INST(CheckRequiredOrDefault, const UniValue&, *CHECK_NONFATAL(maybe_arg););
|
|||||||
TMPL_INST(CheckRequiredOrDefault, bool, CHECK_NONFATAL(maybe_arg)->get_bool(););
|
TMPL_INST(CheckRequiredOrDefault, bool, CHECK_NONFATAL(maybe_arg)->get_bool(););
|
||||||
TMPL_INST(CheckRequiredOrDefault, int, CHECK_NONFATAL(maybe_arg)->getInt<int>(););
|
TMPL_INST(CheckRequiredOrDefault, int, CHECK_NONFATAL(maybe_arg)->getInt<int>(););
|
||||||
TMPL_INST(CheckRequiredOrDefault, uint64_t, CHECK_NONFATAL(maybe_arg)->getInt<uint64_t>(););
|
TMPL_INST(CheckRequiredOrDefault, uint64_t, CHECK_NONFATAL(maybe_arg)->getInt<uint64_t>(););
|
||||||
|
TMPL_INST(CheckRequiredOrDefault, uint32_t, CHECK_NONFATAL(maybe_arg)->getInt<uint32_t>(););
|
||||||
TMPL_INST(CheckRequiredOrDefault, const std::string&, CHECK_NONFATAL(maybe_arg)->get_str(););
|
TMPL_INST(CheckRequiredOrDefault, const std::string&, CHECK_NONFATAL(maybe_arg)->get_str(););
|
||||||
|
|
||||||
bool RPCHelpMan::IsValidNumArgs(size_t num_args) const
|
bool RPCHelpMan::IsValidNumArgs(size_t num_args) const
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ namespace wallet {
|
|||||||
const int DEFAULT_MIN_DEPTH = 0;
|
const int DEFAULT_MIN_DEPTH = 0;
|
||||||
const int DEFAULT_MAX_DEPTH = 9999999;
|
const int DEFAULT_MAX_DEPTH = 9999999;
|
||||||
|
|
||||||
|
const int DEFAULT_WALLET_TX_VERSION = CTransaction::CURRENT_VERSION;
|
||||||
|
|
||||||
//! Default for -avoidpartialspends
|
//! Default for -avoidpartialspends
|
||||||
static constexpr bool DEFAULT_AVOIDPARTIALSPENDS = false;
|
static constexpr bool DEFAULT_AVOIDPARTIALSPENDS = false;
|
||||||
|
|
||||||
@@ -109,10 +111,10 @@ public:
|
|||||||
int m_max_depth = DEFAULT_MAX_DEPTH;
|
int m_max_depth = DEFAULT_MAX_DEPTH;
|
||||||
//! SigningProvider that has pubkeys and scripts to do spend size estimation for external inputs
|
//! SigningProvider that has pubkeys and scripts to do spend size estimation for external inputs
|
||||||
FlatSigningProvider m_external_provider;
|
FlatSigningProvider m_external_provider;
|
||||||
|
//! Version
|
||||||
|
uint32_t m_version = DEFAULT_WALLET_TX_VERSION;
|
||||||
//! Locktime
|
//! Locktime
|
||||||
std::optional<uint32_t> m_locktime;
|
std::optional<uint32_t> m_locktime;
|
||||||
//! Version
|
|
||||||
std::optional<uint32_t> m_version;
|
|
||||||
//! Caps weight of resulting tx
|
//! Caps weight of resulting tx
|
||||||
std::optional<int> m_max_tx_weight{std::nullopt};
|
std::optional<int> m_max_tx_weight{std::nullopt};
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ struct CoinSelectionParams {
|
|||||||
* 1) Received from other wallets, 2) replacing other txs, 3) that have been replaced.
|
* 1) Received from other wallets, 2) replacing other txs, 3) that have been replaced.
|
||||||
*/
|
*/
|
||||||
bool m_include_unsafe_inputs = false;
|
bool m_include_unsafe_inputs = false;
|
||||||
|
/** The version of the transaction we are trying to create. */
|
||||||
|
uint32_t m_version{CTransaction::CURRENT_VERSION};
|
||||||
/** The maximum weight for this transaction. */
|
/** The maximum weight for this transaction. */
|
||||||
std::optional<int> m_max_tx_weight{std::nullopt};
|
std::optional<int> m_max_tx_weight{std::nullopt};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <key_io.h>
|
#include <key_io.h>
|
||||||
#include <node/types.h>
|
#include <node/types.h>
|
||||||
#include <policy/policy.h>
|
#include <policy/policy.h>
|
||||||
|
#include <policy/truc_policy.h>
|
||||||
#include <rpc/rawtransaction_util.h>
|
#include <rpc/rawtransaction_util.h>
|
||||||
#include <rpc/util.h>
|
#include <rpc/util.h>
|
||||||
#include <script/script.h>
|
#include <script/script.h>
|
||||||
@@ -717,6 +718,12 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
|
|||||||
coinControl.m_max_tx_weight = options["max_tx_weight"].getInt<int>();
|
coinControl.m_max_tx_weight = options["max_tx_weight"].getInt<int>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tx.version == TRUC_VERSION) {
|
||||||
|
if (!coinControl.m_max_tx_weight.has_value() || coinControl.m_max_tx_weight.value() > TRUC_MAX_WEIGHT) {
|
||||||
|
coinControl.m_max_tx_weight = TRUC_MAX_WEIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (recipients.empty())
|
if (recipients.empty())
|
||||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "TX must have at least one output");
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "TX must have at least one output");
|
||||||
|
|
||||||
@@ -1269,6 +1276,7 @@ RPCHelpMan send()
|
|||||||
},
|
},
|
||||||
FundTxDoc()),
|
FundTxDoc()),
|
||||||
RPCArgOptions{.oneline_description="options"}},
|
RPCArgOptions{.oneline_description="options"}},
|
||||||
|
{"version", RPCArg::Type::NUM, RPCArg::Default{DEFAULT_WALLET_TX_VERSION}, "Transaction version"},
|
||||||
},
|
},
|
||||||
RPCResult{
|
RPCResult{
|
||||||
RPCResult::Type::OBJ, "", "",
|
RPCResult::Type::OBJ, "", "",
|
||||||
@@ -1308,14 +1316,16 @@ RPCHelpMan send()
|
|||||||
ParseOutputs(outputs),
|
ParseOutputs(outputs),
|
||||||
InterpretSubtractFeeFromOutputInstructions(options["subtract_fee_from_outputs"], outputs.getKeys())
|
InterpretSubtractFeeFromOutputInstructions(options["subtract_fee_from_outputs"], outputs.getKeys())
|
||||||
);
|
);
|
||||||
CMutableTransaction rawTx = ConstructTransaction(options["inputs"], request.params[0], options["locktime"], rbf);
|
|
||||||
CCoinControl coin_control;
|
CCoinControl coin_control;
|
||||||
|
coin_control.m_version = self.Arg<uint32_t>("version");
|
||||||
|
CMutableTransaction rawTx = ConstructTransaction(options["inputs"], request.params[0], options["locktime"], rbf, coin_control.m_version);
|
||||||
// Automatically select coins, unless at least one is manually selected. Can
|
// Automatically select coins, unless at least one is manually selected. Can
|
||||||
// be overridden by options.add_inputs.
|
// be overridden by options.add_inputs.
|
||||||
coin_control.m_allow_other_inputs = rawTx.vin.size() == 0;
|
coin_control.m_allow_other_inputs = rawTx.vin.size() == 0;
|
||||||
if (options.exists("max_tx_weight")) {
|
if (options.exists("max_tx_weight")) {
|
||||||
coin_control.m_max_tx_weight = options["max_tx_weight"].getInt<int>();
|
coin_control.m_max_tx_weight = options["max_tx_weight"].getInt<int>();
|
||||||
}
|
}
|
||||||
|
|
||||||
SetOptionsInputWeights(options["inputs"], options);
|
SetOptionsInputWeights(options["inputs"], options);
|
||||||
// Clear tx.vout since it is not meant to be used now that we are passing outputs directly.
|
// Clear tx.vout since it is not meant to be used now that we are passing outputs directly.
|
||||||
// This sets us up for a future PR to completely remove tx from the function signature in favor of passing inputs directly
|
// This sets us up for a future PR to completely remove tx from the function signature in favor of passing inputs directly
|
||||||
@@ -1375,6 +1385,7 @@ RPCHelpMan sendall()
|
|||||||
{"send_max", RPCArg::Type::BOOL, RPCArg::Default{false}, "When true, only use UTXOs that can pay for their own fees to maximize the output amount. When 'false' (default), no UTXO is left behind. send_max is incompatible with providing specific inputs."},
|
{"send_max", RPCArg::Type::BOOL, RPCArg::Default{false}, "When true, only use UTXOs that can pay for their own fees to maximize the output amount. When 'false' (default), no UTXO is left behind. send_max is incompatible with providing specific inputs."},
|
||||||
{"minconf", RPCArg::Type::NUM, RPCArg::Default{0}, "Require inputs with at least this many confirmations."},
|
{"minconf", RPCArg::Type::NUM, RPCArg::Default{0}, "Require inputs with at least this many confirmations."},
|
||||||
{"maxconf", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Require inputs with at most this many confirmations."},
|
{"maxconf", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Require inputs with at most this many confirmations."},
|
||||||
|
{"version", RPCArg::Type::NUM, RPCArg::Default{DEFAULT_WALLET_TX_VERSION}, "Transaction version"},
|
||||||
},
|
},
|
||||||
FundTxDoc()
|
FundTxDoc()
|
||||||
),
|
),
|
||||||
@@ -1455,6 +1466,16 @@ RPCHelpMan sendall()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.exists("version")) {
|
||||||
|
coin_control.m_version = options["version"].getInt<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coin_control.m_version == TRUC_VERSION) {
|
||||||
|
coin_control.m_max_tx_weight = TRUC_MAX_WEIGHT;
|
||||||
|
} else {
|
||||||
|
coin_control.m_max_tx_weight = MAX_STANDARD_TX_WEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
const bool rbf{options.exists("replaceable") ? options["replaceable"].get_bool() : pwallet->m_signal_rbf};
|
const bool rbf{options.exists("replaceable") ? options["replaceable"].get_bool() : pwallet->m_signal_rbf};
|
||||||
|
|
||||||
FeeCalculation fee_calc_out;
|
FeeCalculation fee_calc_out;
|
||||||
@@ -1469,7 +1490,7 @@ RPCHelpMan sendall()
|
|||||||
throw JSONRPCError(RPC_WALLET_ERROR, "Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee.");
|
throw JSONRPCError(RPC_WALLET_ERROR, "Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee.");
|
||||||
}
|
}
|
||||||
|
|
||||||
CMutableTransaction rawTx{ConstructTransaction(options["inputs"], recipient_key_value_pairs, options["locktime"], rbf)};
|
CMutableTransaction rawTx{ConstructTransaction(options["inputs"], recipient_key_value_pairs, options["locktime"], rbf, coin_control.m_version)};
|
||||||
LOCK(pwallet->cs_wallet);
|
LOCK(pwallet->cs_wallet);
|
||||||
|
|
||||||
CAmount total_input_value(0);
|
CAmount total_input_value(0);
|
||||||
@@ -1487,6 +1508,13 @@ RPCHelpMan sendall()
|
|||||||
if (!tx || input.prevout.n >= tx->tx->vout.size() || !(pwallet->IsMine(tx->tx->vout[input.prevout.n]) & ISMINE_SPENDABLE)) {
|
if (!tx || input.prevout.n >= tx->tx->vout.size() || !(pwallet->IsMine(tx->tx->vout[input.prevout.n]) & ISMINE_SPENDABLE)) {
|
||||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Input not found. UTXO (%s:%d) is not part of wallet.", input.prevout.hash.ToString(), input.prevout.n));
|
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Input not found. UTXO (%s:%d) is not part of wallet.", input.prevout.hash.ToString(), input.prevout.n));
|
||||||
}
|
}
|
||||||
|
if (pwallet->GetTxDepthInMainChain(*tx) == 0) {
|
||||||
|
if (tx->tx->version == TRUC_VERSION && coin_control.m_version != TRUC_VERSION) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Can't spend unconfirmed version 3 pre-selected input with a version %d tx", coin_control.m_version));
|
||||||
|
} else if (coin_control.m_version == TRUC_VERSION && tx->tx->version != TRUC_VERSION) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Can't spend unconfirmed version %d pre-selected input with a version 3 tx", tx->tx->version));
|
||||||
|
}
|
||||||
|
}
|
||||||
total_input_value += tx->tx->vout[input.prevout.n].nValue;
|
total_input_value += tx->tx->vout[input.prevout.n].nValue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1497,6 +1525,10 @@ RPCHelpMan sendall()
|
|||||||
if (send_max && fee_rate.GetFee(output.input_bytes) > output.txout.nValue) {
|
if (send_max && fee_rate.GetFee(output.input_bytes) > output.txout.nValue) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// we are spending an unconfirmed TRUC transaction, so lower max weight
|
||||||
|
if (output.depth == 0 && coin_control.m_version == TRUC_VERSION) {
|
||||||
|
coin_control.m_max_tx_weight = TRUC_CHILD_MAX_WEIGHT;
|
||||||
|
}
|
||||||
CTxIn input(output.outpoint.hash, output.outpoint.n, CScript(), rbf ? MAX_BIP125_RBF_SEQUENCE : CTxIn::SEQUENCE_FINAL);
|
CTxIn input(output.outpoint.hash, output.outpoint.n, CScript(), rbf ? MAX_BIP125_RBF_SEQUENCE : CTxIn::SEQUENCE_FINAL);
|
||||||
rawTx.vin.push_back(input);
|
rawTx.vin.push_back(input);
|
||||||
total_input_value += output.txout.nValue;
|
total_input_value += output.txout.nValue;
|
||||||
@@ -1529,7 +1561,7 @@ RPCHelpMan sendall()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If this transaction is too large, e.g. because the wallet has many UTXOs, it will be rejected by the node's mempool.
|
// If this transaction is too large, e.g. because the wallet has many UTXOs, it will be rejected by the node's mempool.
|
||||||
if (tx_size.weight > MAX_STANDARD_TX_WEIGHT) {
|
if (tx_size.weight > coin_control.m_max_tx_weight) {
|
||||||
throw JSONRPCError(RPC_WALLET_ERROR, "Transaction too large.");
|
throw JSONRPCError(RPC_WALLET_ERROR, "Transaction too large.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1731,6 +1763,7 @@ RPCHelpMan walletcreatefundedpsbt()
|
|||||||
FundTxDoc()),
|
FundTxDoc()),
|
||||||
RPCArgOptions{.oneline_description="options"}},
|
RPCArgOptions{.oneline_description="options"}},
|
||||||
{"bip32derivs", RPCArg::Type::BOOL, RPCArg::Default{true}, "Include BIP 32 derivation paths for public keys if we know them"},
|
{"bip32derivs", RPCArg::Type::BOOL, RPCArg::Default{true}, "Include BIP 32 derivation paths for public keys if we know them"},
|
||||||
|
{"version", RPCArg::Type::NUM, RPCArg::Default{DEFAULT_WALLET_TX_VERSION}, "Transaction version"},
|
||||||
},
|
},
|
||||||
RPCResult{
|
RPCResult{
|
||||||
RPCResult::Type::OBJ, "", "",
|
RPCResult::Type::OBJ, "", "",
|
||||||
@@ -1758,16 +1791,18 @@ RPCHelpMan walletcreatefundedpsbt()
|
|||||||
|
|
||||||
UniValue options{request.params[3].isNull() ? UniValue::VOBJ : request.params[3]};
|
UniValue options{request.params[3].isNull() ? UniValue::VOBJ : request.params[3]};
|
||||||
|
|
||||||
|
CCoinControl coin_control;
|
||||||
|
coin_control.m_version = self.Arg<uint32_t>("version");
|
||||||
|
|
||||||
const UniValue &replaceable_arg = options["replaceable"];
|
const UniValue &replaceable_arg = options["replaceable"];
|
||||||
const bool rbf{replaceable_arg.isNull() ? wallet.m_signal_rbf : replaceable_arg.get_bool()};
|
const bool rbf{replaceable_arg.isNull() ? wallet.m_signal_rbf : replaceable_arg.get_bool()};
|
||||||
CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf);
|
CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf, coin_control.m_version);
|
||||||
UniValue outputs(UniValue::VOBJ);
|
UniValue outputs(UniValue::VOBJ);
|
||||||
outputs = NormalizeOutputs(request.params[1]);
|
outputs = NormalizeOutputs(request.params[1]);
|
||||||
std::vector<CRecipient> recipients = CreateRecipients(
|
std::vector<CRecipient> recipients = CreateRecipients(
|
||||||
ParseOutputs(outputs),
|
ParseOutputs(outputs),
|
||||||
InterpretSubtractFeeFromOutputInstructions(options["subtractFeeFromOutputs"], outputs.getKeys())
|
InterpretSubtractFeeFromOutputInstructions(options["subtractFeeFromOutputs"], outputs.getKeys())
|
||||||
);
|
);
|
||||||
CCoinControl coin_control;
|
|
||||||
// Automatically select coins, unless at least one is manually selected. Can
|
// Automatically select coins, unless at least one is manually selected. Can
|
||||||
// be overridden by options.add_inputs.
|
// be overridden by options.add_inputs.
|
||||||
coin_control.m_allow_other_inputs = rawTx.vin.size() == 0;
|
coin_control.m_allow_other_inputs = rawTx.vin.size() == 0;
|
||||||
|
|||||||
+59
-6
@@ -12,6 +12,7 @@
|
|||||||
#include <node/types.h>
|
#include <node/types.h>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <policy/policy.h>
|
#include <policy/policy.h>
|
||||||
|
#include <policy/truc_policy.h>
|
||||||
#include <primitives/transaction.h>
|
#include <primitives/transaction.h>
|
||||||
#include <primitives/transaction_identifier.h>
|
#include <primitives/transaction_identifier.h>
|
||||||
#include <script/script.h>
|
#include <script/script.h>
|
||||||
@@ -282,6 +283,14 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const
|
|||||||
if (input_bytes == -1) {
|
if (input_bytes == -1) {
|
||||||
input_bytes = CalculateMaximumSignedInputSize(txout, &wallet, &coin_control);
|
input_bytes = CalculateMaximumSignedInputSize(txout, &wallet, &coin_control);
|
||||||
}
|
}
|
||||||
|
const CWalletTx& parent_tx = txo->GetWalletTx();
|
||||||
|
if (wallet.GetTxDepthInMainChain(parent_tx) == 0) {
|
||||||
|
if (parent_tx.tx->version == TRUC_VERSION && coin_control.m_version != TRUC_VERSION) {
|
||||||
|
return util::Error{strprintf(_("Can't spend unconfirmed version 3 pre-selected input with a version %d tx"), coin_control.m_version)};
|
||||||
|
} else if (coin_control.m_version == TRUC_VERSION && parent_tx.tx->version != TRUC_VERSION) {
|
||||||
|
return util::Error{strprintf(_("Can't spend unconfirmed version %d pre-selected input with a version 3 tx"), parent_tx.tx->version)};
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// The input is external. We did not find the tx in mapWallet.
|
// The input is external. We did not find the tx in mapWallet.
|
||||||
const auto out{coin_control.GetExternalOutput(outpoint)};
|
const auto out{coin_control.GetExternalOutput(outpoint)};
|
||||||
@@ -316,6 +325,9 @@ CoinsResult AvailableCoins(const CWallet& wallet,
|
|||||||
AssertLockHeld(wallet.cs_wallet);
|
AssertLockHeld(wallet.cs_wallet);
|
||||||
|
|
||||||
CoinsResult result;
|
CoinsResult result;
|
||||||
|
// track unconfirmed truc outputs separately if we are tracking trucness
|
||||||
|
std::vector<std::pair<OutputType, COutput>> unconfirmed_truc_coins;
|
||||||
|
std::unordered_map<Txid, CAmount, SaltedTxidHasher> truc_txid_by_value;
|
||||||
// Either the WALLET_FLAG_AVOID_REUSE flag is not set (in which case we always allow), or we default to avoiding, and only in the case where
|
// Either the WALLET_FLAG_AVOID_REUSE flag is not set (in which case we always allow), or we default to avoiding, and only in the case where
|
||||||
// a coin control object is provided, and has the avoid address reuse flag set to false, do we allow already used addresses
|
// a coin control object is provided, and has the avoid address reuse flag set to false, do we allow already used addresses
|
||||||
bool allow_used_addresses = !wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE) || (coinControl && !coinControl->m_avoid_address_reuse);
|
bool allow_used_addresses = !wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE) || (coinControl && !coinControl->m_avoid_address_reuse);
|
||||||
@@ -386,6 +398,17 @@ CoinsResult AvailableCoins(const CWallet& wallet,
|
|||||||
safeTx = false;
|
safeTx = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nDepth == 0 && params.check_version_trucness) {
|
||||||
|
if (coinControl->m_version == TRUC_VERSION) {
|
||||||
|
if (wtx.tx->version != TRUC_VERSION) continue;
|
||||||
|
// this unconfirmed v3 transaction already has a child
|
||||||
|
if (wtx.truc_child_in_mempool.has_value()) continue;
|
||||||
|
} else {
|
||||||
|
if (wtx.tx->version == TRUC_VERSION) continue;
|
||||||
|
Assume(!wtx.truc_child_in_mempool.has_value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (only_safe && !safeTx) {
|
if (only_safe && !safeTx) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -450,8 +473,15 @@ CoinsResult AvailableCoins(const CWallet& wallet,
|
|||||||
is_from_p2sh = true;
|
is_from_p2sh = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Add(GetOutputType(type, is_from_p2sh),
|
auto available_output_type = GetOutputType(type, is_from_p2sh);
|
||||||
COutput(outpoint, output, nDepth, input_bytes, spendable, solvable, tx_safe, wtx.GetTxTime(), tx_from_me, feerate));
|
auto available_output = COutput(outpoint, output, nDepth, input_bytes, spendable, solvable, tx_safe, wtx.GetTxTime(), tx_from_me, feerate);
|
||||||
|
if (wtx.tx->version == TRUC_VERSION && nDepth == 0 && params.check_version_trucness) {
|
||||||
|
unconfirmed_truc_coins.emplace_back(available_output_type, available_output);
|
||||||
|
auto [it, _] = truc_txid_by_value.try_emplace(wtx.tx->GetHash(), 0);
|
||||||
|
it->second += output.nValue;
|
||||||
|
} else {
|
||||||
|
result.Add(available_output_type, available_output);
|
||||||
|
}
|
||||||
|
|
||||||
outpoints.push_back(outpoint);
|
outpoints.push_back(outpoint);
|
||||||
|
|
||||||
@@ -468,6 +498,23 @@ CoinsResult AvailableCoins(const CWallet& wallet,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return all the coins from one TRUC transaction, that have the highest value.
|
||||||
|
// This could be improved in the future by encoding these restrictions in
|
||||||
|
// the coin selection itself so that we don't have to filter out
|
||||||
|
// other unconfirmed TRUC coins beforehand.
|
||||||
|
if (params.check_version_trucness && unconfirmed_truc_coins.size() > 0) {
|
||||||
|
auto highest_value_truc_tx = std::max_element(truc_txid_by_value.begin(), truc_txid_by_value.end(), [](const auto& tx1, const auto& tx2){
|
||||||
|
return tx1.second < tx2.second;
|
||||||
|
});
|
||||||
|
|
||||||
|
const Txid& truc_txid = highest_value_truc_tx->first;
|
||||||
|
for (const auto& [type, output] : unconfirmed_truc_coins) {
|
||||||
|
if (output.outpoint.hash == truc_txid) {
|
||||||
|
result.Add(type, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (feerate.has_value()) {
|
if (feerate.has_value()) {
|
||||||
std::map<COutPoint, CAmount> map_of_bump_fees = wallet.chain().calculateIndividualBumpFees(outpoints, feerate.value());
|
std::map<COutPoint, CAmount> map_of_bump_fees = wallet.chain().calculateIndividualBumpFees(outpoints, feerate.value());
|
||||||
|
|
||||||
@@ -484,6 +531,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
|
|||||||
CoinsResult AvailableCoinsListUnspent(const CWallet& wallet, const CCoinControl* coinControl, CoinFilterParams params)
|
CoinsResult AvailableCoinsListUnspent(const CWallet& wallet, const CCoinControl* coinControl, CoinFilterParams params)
|
||||||
{
|
{
|
||||||
params.only_spendable = false;
|
params.only_spendable = false;
|
||||||
|
params.check_version_trucness = false;
|
||||||
return AvailableCoins(wallet, coinControl, /*feerate=*/ std::nullopt, params);
|
return AvailableCoins(wallet, coinControl, /*feerate=*/ std::nullopt, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -904,11 +952,17 @@ util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, Coin
|
|||||||
// If no solution is found, return the first detailed error (if any).
|
// If no solution is found, return the first detailed error (if any).
|
||||||
// future: add "error level" so the worst one can be picked instead.
|
// future: add "error level" so the worst one can be picked instead.
|
||||||
std::vector<util::Result<SelectionResult>> res_detailed_errors;
|
std::vector<util::Result<SelectionResult>> res_detailed_errors;
|
||||||
|
CoinSelectionParams updated_selection_params = coin_selection_params;
|
||||||
for (const auto& select_filter : ordered_filters) {
|
for (const auto& select_filter : ordered_filters) {
|
||||||
auto it = filtered_groups.find(select_filter.filter);
|
auto it = filtered_groups.find(select_filter.filter);
|
||||||
if (it == filtered_groups.end()) continue;
|
if (it == filtered_groups.end()) continue;
|
||||||
|
if (updated_selection_params.m_version == TRUC_VERSION && (select_filter.filter.conf_mine == 0 || select_filter.filter.conf_theirs == 0)) {
|
||||||
|
if (updated_selection_params.m_max_tx_weight > (TRUC_CHILD_MAX_WEIGHT)) {
|
||||||
|
updated_selection_params.m_max_tx_weight = TRUC_CHILD_MAX_WEIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (auto res{AttemptSelection(wallet.chain(), value_to_select, it->second,
|
if (auto res{AttemptSelection(wallet.chain(), value_to_select, it->second,
|
||||||
coin_selection_params, select_filter.allow_mixed_output_types)}) {
|
updated_selection_params, select_filter.allow_mixed_output_types)}) {
|
||||||
return res; // result found
|
return res; // result found
|
||||||
} else {
|
} else {
|
||||||
// If any specific error message appears here, then something particularly wrong might have happened.
|
// If any specific error message appears here, then something particularly wrong might have happened.
|
||||||
@@ -1019,14 +1073,13 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
|
|||||||
FastRandomContext rng_fast;
|
FastRandomContext rng_fast;
|
||||||
CMutableTransaction txNew; // The resulting transaction that we make
|
CMutableTransaction txNew; // The resulting transaction that we make
|
||||||
|
|
||||||
if (coin_control.m_version) {
|
txNew.version = coin_control.m_version;
|
||||||
txNew.version = coin_control.m_version.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
CoinSelectionParams coin_selection_params{rng_fast}; // Parameters for coin selection, init with dummy
|
CoinSelectionParams coin_selection_params{rng_fast}; // Parameters for coin selection, init with dummy
|
||||||
coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends;
|
coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends;
|
||||||
coin_selection_params.m_include_unsafe_inputs = coin_control.m_include_unsafe_inputs;
|
coin_selection_params.m_include_unsafe_inputs = coin_control.m_include_unsafe_inputs;
|
||||||
coin_selection_params.m_max_tx_weight = coin_control.m_max_tx_weight.value_or(MAX_STANDARD_TX_WEIGHT);
|
coin_selection_params.m_max_tx_weight = coin_control.m_max_tx_weight.value_or(MAX_STANDARD_TX_WEIGHT);
|
||||||
|
coin_selection_params.m_version = coin_control.m_version;
|
||||||
int minimum_tx_weight = MIN_STANDARD_TX_NONWITNESS_SIZE * WITNESS_SCALE_FACTOR;
|
int minimum_tx_weight = MIN_STANDARD_TX_NONWITNESS_SIZE * WITNESS_SCALE_FACTOR;
|
||||||
if (coin_selection_params.m_max_tx_weight.value() < minimum_tx_weight || coin_selection_params.m_max_tx_weight.value() > MAX_STANDARD_TX_WEIGHT) {
|
if (coin_selection_params.m_max_tx_weight.value() < minimum_tx_weight || coin_selection_params.m_max_tx_weight.value() > MAX_STANDARD_TX_WEIGHT) {
|
||||||
return util::Error{strprintf(_("Maximum transaction weight must be between %d and %d"), minimum_tx_weight, MAX_STANDARD_TX_WEIGHT)};
|
return util::Error{strprintf(_("Maximum transaction weight must be between %d and %d"), minimum_tx_weight, MAX_STANDARD_TX_WEIGHT)};
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ struct CoinFilterParams {
|
|||||||
bool include_immature_coinbase{false};
|
bool include_immature_coinbase{false};
|
||||||
// By default, skip locked UTXOs
|
// By default, skip locked UTXOs
|
||||||
bool skip_locked{true};
|
bool skip_locked{true};
|
||||||
|
// When true, filter unconfirmed coins by whether their
|
||||||
|
// version's TRUCness matches what is set by CCoinControl.
|
||||||
|
bool check_version_trucness{true};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -258,6 +258,10 @@ public:
|
|||||||
// BlockConflicted.
|
// BlockConflicted.
|
||||||
std::set<Txid> mempool_conflicts;
|
std::set<Txid> mempool_conflicts;
|
||||||
|
|
||||||
|
// Track v3 mempool tx that spends from this tx
|
||||||
|
// so that we don't try to create another unconfirmed child
|
||||||
|
std::optional<Txid> truc_child_in_mempool;
|
||||||
|
|
||||||
template<typename Stream>
|
template<typename Stream>
|
||||||
void Serialize(Stream& s) const
|
void Serialize(Stream& s) const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
#include <node/types.h>
|
#include <node/types.h>
|
||||||
#include <outputtype.h>
|
#include <outputtype.h>
|
||||||
#include <policy/feerate.h>
|
#include <policy/feerate.h>
|
||||||
|
#include <policy/truc_policy.h>
|
||||||
#include <primitives/block.h>
|
#include <primitives/block.h>
|
||||||
#include <primitives/transaction.h>
|
#include <primitives/transaction.h>
|
||||||
#include <psbt.h>
|
#include <psbt.h>
|
||||||
@@ -1213,6 +1214,23 @@ bool CWallet::TransactionCanBeAbandoned(const Txid& hashTx) const
|
|||||||
return wtx && !wtx->isAbandoned() && GetTxDepthInMainChain(*wtx) == 0 && !wtx->InMempool();
|
return wtx && !wtx->isAbandoned() && GetTxDepthInMainChain(*wtx) == 0 && !wtx->InMempool();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CWallet::UpdateTrucSiblingConflicts(const CWalletTx& parent_wtx, const Txid& child_txid, bool add_conflict) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet)
|
||||||
|
{
|
||||||
|
// Find all other txs in our wallet that spend utxos from this parent
|
||||||
|
// so that we can mark them as mempool-conflicted by this new tx.
|
||||||
|
for (long unsigned int i = 0; i < parent_wtx.tx->vout.size(); i++) {
|
||||||
|
for (auto range = mapTxSpends.equal_range(COutPoint(parent_wtx.tx->GetHash(), i)); range.first != range.second; range.first++) {
|
||||||
|
const Txid& sibling_txid = range.first->second;
|
||||||
|
// Skip the child_tx itself
|
||||||
|
if (sibling_txid == child_txid) continue;
|
||||||
|
RecursiveUpdateTxState(/*batch=*/nullptr, sibling_txid, [&child_txid, add_conflict](CWalletTx& wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) {
|
||||||
|
return add_conflict ? (wtx.mempool_conflicts.insert(child_txid).second ? TxUpdate::CHANGED : TxUpdate::UNCHANGED)
|
||||||
|
: (wtx.mempool_conflicts.erase(child_txid) ? TxUpdate::CHANGED : TxUpdate::UNCHANGED);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CWallet::MarkInputsDirty(const CTransactionRef& tx)
|
void CWallet::MarkInputsDirty(const CTransactionRef& tx)
|
||||||
{
|
{
|
||||||
for (const CTxIn& txin : tx->vin) {
|
for (const CTxIn& txin : tx->vin) {
|
||||||
@@ -1368,6 +1386,25 @@ void CWallet::transactionAddedToMempool(const CTransactionRef& tx) {
|
|||||||
return wtx.mempool_conflicts.insert(txid).second ? TxUpdate::CHANGED : TxUpdate::UNCHANGED;
|
return wtx.mempool_conflicts.insert(txid).second ? TxUpdate::CHANGED : TxUpdate::UNCHANGED;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx->version == TRUC_VERSION) {
|
||||||
|
// Unconfirmed TRUC transactions are only allowed a 1-parent-1-child topology.
|
||||||
|
// For any unconfirmed v3 parents (there should be a maximum of 1 except in reorgs),
|
||||||
|
// record this child so the wallet doesn't try to spend any other outputs
|
||||||
|
for (const CTxIn& tx_in : tx->vin) {
|
||||||
|
auto parent_it = mapWallet.find(tx_in.prevout.hash);
|
||||||
|
if (parent_it != mapWallet.end()) {
|
||||||
|
CWalletTx& parent_wtx = parent_it->second;
|
||||||
|
if (parent_wtx.isUnconfirmed()) {
|
||||||
|
parent_wtx.truc_child_in_mempool = tx->GetHash();
|
||||||
|
// Even though these siblings do not spend the same utxos, they can't
|
||||||
|
// be present in the mempool at the same time because of TRUC policy rules
|
||||||
|
UpdateTrucSiblingConflicts(parent_wtx, txid, /*add_conflict=*/true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1421,6 +1458,23 @@ void CWallet::transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRe
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tx->version == TRUC_VERSION) {
|
||||||
|
// If this tx has a parent, unset its truc_child_in_mempool to make it possible
|
||||||
|
// to spend from the parent again. If this tx was replaced by another
|
||||||
|
// child of the same parent, transactionAddedToMempool
|
||||||
|
// will update truc_child_in_mempool
|
||||||
|
for (const CTxIn& tx_in : tx->vin) {
|
||||||
|
auto parent_it = mapWallet.find(tx_in.prevout.hash);
|
||||||
|
if (parent_it != mapWallet.end()) {
|
||||||
|
CWalletTx& parent_wtx = parent_it->second;
|
||||||
|
if (parent_wtx.truc_child_in_mempool == tx->GetHash()) {
|
||||||
|
parent_wtx.truc_child_in_mempool = std::nullopt;
|
||||||
|
UpdateTrucSiblingConflicts(parent_wtx, txid, /*add_conflict=*/false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CWallet::blockConnected(ChainstateRole role, const interfaces::BlockInfo& block)
|
void CWallet::blockConnected(ChainstateRole role, const interfaces::BlockInfo& block)
|
||||||
|
|||||||
@@ -450,6 +450,9 @@ private:
|
|||||||
// Update last block processed in memory only
|
// Update last block processed in memory only
|
||||||
void SetLastBlockProcessedInMem(int block_height, uint256 block_hash) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
void SetLastBlockProcessedInMem(int block_height, uint256 block_hash) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||||
|
|
||||||
|
//! Update mempool conflicts for TRUC sibling transactions
|
||||||
|
void UpdateTrucSiblingConflicts(const CWalletTx& parent_wtx, const Txid& child_txid, bool add_conflict) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* Main wallet lock.
|
* Main wallet lock.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from test_framework.messages import (
|
|||||||
CTxOut,
|
CTxOut,
|
||||||
SEQUENCE_FINAL,
|
SEQUENCE_FINAL,
|
||||||
tx_from_hex,
|
tx_from_hex,
|
||||||
|
TX_MAX_STANDARD_VERSION,
|
||||||
WITNESS_SCALE_FACTOR,
|
WITNESS_SCALE_FACTOR,
|
||||||
)
|
)
|
||||||
from test_framework.script import (
|
from test_framework.script import (
|
||||||
@@ -666,7 +667,6 @@ SIG_ADD_ZERO = {"failure": {"sign": zero_appender(default_sign)}}
|
|||||||
DUST_LIMIT = 600
|
DUST_LIMIT = 600
|
||||||
MIN_FEE = 50000
|
MIN_FEE = 50000
|
||||||
|
|
||||||
TX_MAX_STANDARD_VERSION = 3
|
|
||||||
TX_STANDARD_VERSIONS = [1, 2, TX_MAX_STANDARD_VERSION]
|
TX_STANDARD_VERSIONS = [1, 2, TX_MAX_STANDARD_VERSION]
|
||||||
TRUC_MAX_VSIZE = 10000 # test doesn't cover in-mempool spends, so only this limit is hit
|
TRUC_MAX_VSIZE = 10000 # test doesn't cover in-mempool spends, so only this limit is hit
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ from itertools import product
|
|||||||
from test_framework.messages import (
|
from test_framework.messages import (
|
||||||
MAX_BIP125_RBF_SEQUENCE,
|
MAX_BIP125_RBF_SEQUENCE,
|
||||||
COIN,
|
COIN,
|
||||||
|
TX_MAX_STANDARD_VERSION,
|
||||||
|
TX_MIN_STANDARD_VERSION,
|
||||||
CTransaction,
|
CTransaction,
|
||||||
CTxOut,
|
CTxOut,
|
||||||
tx_from_hex,
|
tx_from_hex,
|
||||||
@@ -254,7 +256,11 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||||||
assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, [])
|
assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, [])
|
||||||
|
|
||||||
# Test `createrawtransaction` invalid extra parameters
|
# Test `createrawtransaction` invalid extra parameters
|
||||||
assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, [], {}, 0, False, 'foo')
|
assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, [], {}, 0, False, 2, 3, 'foo')
|
||||||
|
|
||||||
|
# Test `createrawtransaction` invalid version parameters
|
||||||
|
assert_raises_rpc_error(-8, f"Invalid parameter, version out of range({TX_MIN_STANDARD_VERSION}~{TX_MAX_STANDARD_VERSION})", self.nodes[0].createrawtransaction, [], {}, 0, False, TX_MIN_STANDARD_VERSION - 1)
|
||||||
|
assert_raises_rpc_error(-8, f"Invalid parameter, version out of range({TX_MIN_STANDARD_VERSION}~{TX_MAX_STANDARD_VERSION})", self.nodes[0].createrawtransaction, [], {}, 0, False, TX_MAX_STANDARD_VERSION + 1)
|
||||||
|
|
||||||
# Test `createrawtransaction` invalid `inputs`
|
# Test `createrawtransaction` invalid `inputs`
|
||||||
assert_raises_rpc_error(-3, "JSON value of type string is not of expected type array", self.nodes[0].createrawtransaction, 'foo', {})
|
assert_raises_rpc_error(-3, "JSON value of type string is not of expected type array", self.nodes[0].createrawtransaction, 'foo', {})
|
||||||
@@ -334,6 +340,11 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||||||
self.nodes[2].createrawtransaction(inputs=[{'txid': TXID, 'vout': 9}], outputs=[{address: 99}, {address2: 99}, {'data': '99'}]),
|
self.nodes[2].createrawtransaction(inputs=[{'txid': TXID, 'vout': 9}], outputs=[{address: 99}, {address2: 99}, {'data': '99'}]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for version in range(TX_MIN_STANDARD_VERSION, TX_MAX_STANDARD_VERSION + 1):
|
||||||
|
rawtx = self.nodes[2].createrawtransaction(inputs=[{'txid': TXID, 'vout': 9}], outputs=OrderedDict([(address, 99), (address2, 99)]), version=version)
|
||||||
|
tx = tx_from_hex(rawtx)
|
||||||
|
assert_equal(tx.version, version)
|
||||||
|
|
||||||
def sendrawtransaction_tests(self):
|
def sendrawtransaction_tests(self):
|
||||||
self.log.info("Test sendrawtransaction with missing input")
|
self.log.info("Test sendrawtransaction with missing input")
|
||||||
inputs = [{'txid': TXID, 'vout': 1}] # won't exist
|
inputs = [{'txid': TXID, 'vout': 1}] # won't exist
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ DEFAULT_MIN_RELAY_TX_FEE = 100
|
|||||||
# Default for -incrementalrelayfee in sat/kvB
|
# Default for -incrementalrelayfee in sat/kvB
|
||||||
DEFAULT_INCREMENTAL_RELAY_FEE = 100
|
DEFAULT_INCREMENTAL_RELAY_FEE = 100
|
||||||
|
|
||||||
|
TRUC_MAX_VSIZE = 10000
|
||||||
|
TRUC_CHILD_MAX_VSIZE = 1000
|
||||||
|
|
||||||
def assert_mempool_contents(test_framework, node, expected=None, sync=True):
|
def assert_mempool_contents(test_framework, node, expected=None, sync=True):
|
||||||
"""Assert that all transactions in expected are in the mempool,
|
"""Assert that all transactions in expected are in the mempool,
|
||||||
and no additional ones exist. 'expected' is an array of
|
and no additional ones exist. 'expected' is an array of
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ MAX_OP_RETURN_RELAY = 100_000
|
|||||||
|
|
||||||
DEFAULT_MEMPOOL_EXPIRY_HOURS = 336 # hours
|
DEFAULT_MEMPOOL_EXPIRY_HOURS = 336 # hours
|
||||||
|
|
||||||
|
TX_MIN_STANDARD_VERSION = 1
|
||||||
|
TX_MAX_STANDARD_VERSION = 3
|
||||||
|
|
||||||
MAGIC_BYTES = {
|
MAGIC_BYTES = {
|
||||||
"mainnet": b"\xf9\xbe\xb4\xd9",
|
"mainnet": b"\xf9\xbe\xb4\xd9",
|
||||||
"testnet4": b"\x1c\x16\x3f\x28",
|
"testnet4": b"\x1c\x16\x3f\x28",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from test_framework.messages import (
|
|||||||
CTxIn,
|
CTxIn,
|
||||||
CTxInWitness,
|
CTxInWitness,
|
||||||
CTxOut,
|
CTxOut,
|
||||||
|
ser_compact_size,
|
||||||
sha256,
|
sha256,
|
||||||
)
|
)
|
||||||
from test_framework.script import (
|
from test_framework.script import (
|
||||||
@@ -35,6 +36,8 @@ from test_framework.script import (
|
|||||||
hash160,
|
hash160,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from test_framework.util import assert_equal
|
||||||
|
|
||||||
# Maximum number of potentially executed legacy signature operations in validating a transaction.
|
# Maximum number of potentially executed legacy signature operations in validating a transaction.
|
||||||
MAX_STD_LEGACY_SIGOPS = 2_500
|
MAX_STD_LEGACY_SIGOPS = 2_500
|
||||||
|
|
||||||
@@ -128,6 +131,16 @@ def script_to_p2sh_p2wsh_script(script):
|
|||||||
p2shscript = CScript([OP_0, sha256(script)])
|
p2shscript = CScript([OP_0, sha256(script)])
|
||||||
return script_to_p2sh_script(p2shscript)
|
return script_to_p2sh_script(p2shscript)
|
||||||
|
|
||||||
|
def bulk_vout(tx, target_vsize):
|
||||||
|
if target_vsize < tx.get_vsize():
|
||||||
|
raise RuntimeError(f"target_vsize {target_vsize} is less than transaction virtual size {tx.get_vsize()}")
|
||||||
|
# determine number of needed padding bytes
|
||||||
|
dummy_vbytes = target_vsize - tx.get_vsize()
|
||||||
|
# compensate for the increase of the compact-size encoded script length
|
||||||
|
# (note that the length encoding of the unpadded output script needs one byte)
|
||||||
|
dummy_vbytes -= len(ser_compact_size(dummy_vbytes)) - 1
|
||||||
|
tx.vout[-1].scriptPubKey = CScript([OP_RETURN] + [OP_1] * dummy_vbytes)
|
||||||
|
assert_equal(tx.get_vsize(), target_vsize)
|
||||||
|
|
||||||
def output_key_to_p2tr_script(key):
|
def output_key_to_p2tr_script(key):
|
||||||
assert len(key) == 32
|
assert len(key) == 32
|
||||||
|
|||||||
@@ -33,11 +33,9 @@ from test_framework.messages import (
|
|||||||
CTxInWitness,
|
CTxInWitness,
|
||||||
CTxOut,
|
CTxOut,
|
||||||
hash256,
|
hash256,
|
||||||
ser_compact_size,
|
|
||||||
)
|
)
|
||||||
from test_framework.script import (
|
from test_framework.script import (
|
||||||
CScript,
|
CScript,
|
||||||
OP_1,
|
|
||||||
OP_NOP,
|
OP_NOP,
|
||||||
OP_RETURN,
|
OP_RETURN,
|
||||||
OP_TRUE,
|
OP_TRUE,
|
||||||
@@ -45,6 +43,7 @@ from test_framework.script import (
|
|||||||
taproot_construct,
|
taproot_construct,
|
||||||
)
|
)
|
||||||
from test_framework.script_util import (
|
from test_framework.script_util import (
|
||||||
|
bulk_vout,
|
||||||
key_to_p2pk_script,
|
key_to_p2pk_script,
|
||||||
key_to_p2pkh_script,
|
key_to_p2pkh_script,
|
||||||
key_to_p2sh_p2wpkh_script,
|
key_to_p2sh_p2wpkh_script,
|
||||||
@@ -121,17 +120,9 @@ class MiniWallet:
|
|||||||
"""Pad a transaction with extra outputs until it reaches a target vsize.
|
"""Pad a transaction with extra outputs until it reaches a target vsize.
|
||||||
returns the tx
|
returns the tx
|
||||||
"""
|
"""
|
||||||
if target_vsize < tx.get_vsize():
|
|
||||||
raise RuntimeError(f"target_vsize {target_vsize} is less than transaction virtual size {tx.get_vsize()}")
|
|
||||||
|
|
||||||
tx.vout.append(CTxOut(nValue=0, scriptPubKey=CScript([OP_RETURN])))
|
tx.vout.append(CTxOut(nValue=0, scriptPubKey=CScript([OP_RETURN])))
|
||||||
# determine number of needed padding bytes
|
bulk_vout(tx, target_vsize)
|
||||||
dummy_vbytes = target_vsize - tx.get_vsize()
|
|
||||||
# compensate for the increase of the compact-size encoded script length
|
|
||||||
# (note that the length encoding of the unpadded output script needs one byte)
|
|
||||||
dummy_vbytes -= len(ser_compact_size(dummy_vbytes)) - 1
|
|
||||||
tx.vout[-1].scriptPubKey = CScript([OP_RETURN] + [OP_1] * dummy_vbytes)
|
|
||||||
assert_equal(tx.get_vsize(), target_vsize)
|
|
||||||
|
|
||||||
def get_balance(self):
|
def get_balance(self):
|
||||||
return sum(u['value'] for u in self._utxos)
|
return sum(u['value'] for u in self._utxos)
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ BASE_SCRIPTS = [
|
|||||||
'rpc_psbt.py',
|
'rpc_psbt.py',
|
||||||
'wallet_fundrawtransaction.py',
|
'wallet_fundrawtransaction.py',
|
||||||
'wallet_bumpfee.py',
|
'wallet_bumpfee.py',
|
||||||
|
'wallet_v3_txs.py',
|
||||||
'wallet_backup.py',
|
'wallet_backup.py',
|
||||||
'feature_segwit.py --v2transport',
|
'feature_segwit.py --v2transport',
|
||||||
'feature_segwit.py --v1transport',
|
'feature_segwit.py --v1transport',
|
||||||
|
|||||||
Executable
+589
@@ -0,0 +1,589 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2025 The Bitcoin Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
"""Test how the wallet deals with TRUC transactions"""
|
||||||
|
|
||||||
|
from decimal import Decimal, getcontext
|
||||||
|
|
||||||
|
from test_framework.authproxy import JSONRPCException
|
||||||
|
from test_framework.messages import (
|
||||||
|
COIN,
|
||||||
|
CTransaction,
|
||||||
|
CTxOut,
|
||||||
|
)
|
||||||
|
from test_framework.script import (
|
||||||
|
CScript,
|
||||||
|
OP_RETURN
|
||||||
|
)
|
||||||
|
|
||||||
|
from test_framework.script_util import bulk_vout
|
||||||
|
|
||||||
|
from test_framework.test_framework import BitcoinTestFramework
|
||||||
|
from test_framework.util import (
|
||||||
|
assert_equal,
|
||||||
|
assert_greater_than,
|
||||||
|
assert_raises_rpc_error,
|
||||||
|
)
|
||||||
|
|
||||||
|
from test_framework.mempool_util import (
|
||||||
|
TRUC_MAX_VSIZE,
|
||||||
|
TRUC_CHILD_MAX_VSIZE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# sweep alice and bob's wallets and clear the mempool
|
||||||
|
def cleanup(func):
|
||||||
|
def wrapper(self, *args):
|
||||||
|
try:
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
func(self, *args)
|
||||||
|
finally:
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
try:
|
||||||
|
self.alice.sendall([self.charlie.getnewaddress()])
|
||||||
|
except JSONRPCException as e:
|
||||||
|
assert "Total value of UTXO pool too low to pay for transaction" in e.error['message']
|
||||||
|
try:
|
||||||
|
self.bob.sendall([self.charlie.getnewaddress()])
|
||||||
|
except JSONRPCException as e:
|
||||||
|
assert "Total value of UTXO pool too low to pay for transaction" in e.error['message']
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
|
||||||
|
for wallet in [self.alice, self.bob]:
|
||||||
|
balance = wallet.getbalances()["mine"]
|
||||||
|
for balance_type in ["untrusted_pending", "trusted", "immature"]:
|
||||||
|
assert_equal(balance[balance_type], 0)
|
||||||
|
|
||||||
|
assert_equal(self.alice.getrawmempool(), [])
|
||||||
|
assert_equal(self.bob.getrawmempool(), [])
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
class WalletV3Test(BitcoinTestFramework):
|
||||||
|
def skip_test_if_missing_module(self):
|
||||||
|
self.skip_if_no_wallet()
|
||||||
|
|
||||||
|
def set_test_params(self):
|
||||||
|
getcontext().prec=10
|
||||||
|
self.num_nodes = 1
|
||||||
|
self.setup_clean_chain = True
|
||||||
|
|
||||||
|
def send_tx(self, from_wallet, inputs, outputs, version):
|
||||||
|
raw_tx = from_wallet.createrawtransaction(inputs=inputs, outputs=outputs, version=version)
|
||||||
|
if inputs == []:
|
||||||
|
raw_tx = from_wallet.fundrawtransaction(raw_tx, {'include_unsafe' : True})["hex"]
|
||||||
|
raw_tx = from_wallet.signrawtransactionwithwallet(raw_tx)["hex"]
|
||||||
|
txid = from_wallet.sendrawtransaction(raw_tx)
|
||||||
|
return txid
|
||||||
|
|
||||||
|
def bulk_tx(self, tx, amount, target_vsize):
|
||||||
|
tx.vout.append(CTxOut(nValue=(amount * COIN), scriptPubKey=CScript([OP_RETURN])))
|
||||||
|
bulk_vout(tx, target_vsize)
|
||||||
|
|
||||||
|
def run_test_with_swapped_versions(self, test_func):
|
||||||
|
test_func(2, 3)
|
||||||
|
test_func(3, 2)
|
||||||
|
|
||||||
|
def run_test(self):
|
||||||
|
self.nodes[0].createwallet("alice")
|
||||||
|
self.alice = self.nodes[0].get_wallet_rpc("alice")
|
||||||
|
|
||||||
|
self.nodes[0].createwallet("bob")
|
||||||
|
self.bob = self.nodes[0].get_wallet_rpc("bob")
|
||||||
|
|
||||||
|
self.nodes[0].createwallet("charlie")
|
||||||
|
self.charlie = self.nodes[0].get_wallet_rpc("charlie")
|
||||||
|
|
||||||
|
self.generatetoaddress(self.nodes[0], 100, self.charlie.getnewaddress())
|
||||||
|
|
||||||
|
self.run_test_with_swapped_versions(self.tx_spends_unconfirmed_tx_with_wrong_version)
|
||||||
|
self.run_test_with_swapped_versions(self.va_tx_spends_confirmed_vb_tx)
|
||||||
|
self.run_test_with_swapped_versions(self.spend_inputs_with_different_versions)
|
||||||
|
self.spend_inputs_with_different_versions_default_version()
|
||||||
|
self.v3_utxos_appear_in_listunspent()
|
||||||
|
self.truc_tx_with_conflicting_sibling()
|
||||||
|
self.truc_tx_with_conflicting_sibling_change()
|
||||||
|
self.v3_tx_evicted_from_mempool_by_sibling()
|
||||||
|
self.v3_conflict_removed_from_mempool()
|
||||||
|
self.mempool_conflicts_removed_when_v3_conflict_removed()
|
||||||
|
self.max_tx_weight()
|
||||||
|
self.max_tx_child_weight()
|
||||||
|
self.user_input_weight_not_overwritten()
|
||||||
|
self.user_input_weight_not_overwritten_v3_child()
|
||||||
|
self.createpsbt_v3()
|
||||||
|
self.send_v3()
|
||||||
|
self.sendall_v3()
|
||||||
|
self.sendall_with_unconfirmed_v3()
|
||||||
|
self.walletcreatefundedpsbt_v3()
|
||||||
|
self.sendall_truc_weight_limit()
|
||||||
|
self.sendall_truc_child_weight_limit()
|
||||||
|
self.mix_non_truc_versions()
|
||||||
|
self.cant_spend_multiple_unconfirmed_truc_outputs()
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def tx_spends_unconfirmed_tx_with_wrong_version(self, version_a, version_b):
|
||||||
|
self.log.info(f"Test unavailable funds when v{version_b} tx spends unconfirmed v{version_a} tx")
|
||||||
|
|
||||||
|
outputs = {self.bob.getnewaddress() : 2.0}
|
||||||
|
self.send_tx(self.charlie, [], outputs, version_a)
|
||||||
|
|
||||||
|
assert_equal(self.bob.getbalances()["mine"]["trusted"], 0)
|
||||||
|
assert_greater_than(self.bob.getbalances()["mine"]["untrusted_pending"], 0)
|
||||||
|
|
||||||
|
outputs = {self.alice.getnewaddress() : 1.0}
|
||||||
|
|
||||||
|
raw_tx_v2 = self.bob.createrawtransaction(inputs=[], outputs=outputs, version=version_b)
|
||||||
|
|
||||||
|
assert_raises_rpc_error(
|
||||||
|
-4,
|
||||||
|
"Insufficient funds",
|
||||||
|
self.bob.fundrawtransaction,
|
||||||
|
raw_tx_v2, {'include_unsafe': True}
|
||||||
|
)
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def va_tx_spends_confirmed_vb_tx(self, version_a, version_b):
|
||||||
|
self.log.info(f"Test available funds when v{version_b} tx spends confirmed v{version_a} tx")
|
||||||
|
|
||||||
|
outputs = {self.bob.getnewaddress() : 2.0}
|
||||||
|
self.send_tx(self.charlie, [], outputs, version_a)
|
||||||
|
|
||||||
|
assert_equal(self.bob.getbalances()["mine"]["trusted"], 0)
|
||||||
|
assert_greater_than(self.bob.getbalances()["mine"]["untrusted_pending"], 0)
|
||||||
|
|
||||||
|
outputs = {self.alice.getnewaddress() : 1.0}
|
||||||
|
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
|
||||||
|
self.send_tx(self.bob, [], outputs, version_b)
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def v3_utxos_appear_in_listunspent(self):
|
||||||
|
self.log.info("Test that unconfirmed v3 utxos still appear in listunspent")
|
||||||
|
|
||||||
|
outputs = {self.alice.getnewaddress() : 2.0}
|
||||||
|
parent_txid = self.send_tx(self.charlie, [], outputs, 3)
|
||||||
|
assert_equal(self.alice.listunspent(minconf=0)[0]["txid"], parent_txid)
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def truc_tx_with_conflicting_sibling(self):
|
||||||
|
self.log.info("Test v3 transaction with conflicting sibling")
|
||||||
|
|
||||||
|
# unconfirmed v3 tx to alice & bob
|
||||||
|
outputs = {self.alice.getnewaddress() : 2.0, self.bob.getnewaddress() : 2.0}
|
||||||
|
self.send_tx(self.charlie, [], outputs, 3)
|
||||||
|
|
||||||
|
# alice spends her output with a v3 transaction
|
||||||
|
alice_unspent = self.alice.listunspent(minconf=0)[0]
|
||||||
|
outputs = {self.alice.getnewaddress() : alice_unspent['amount'] - Decimal(0.00000120)}
|
||||||
|
self.send_tx(self.alice, [alice_unspent], outputs, 3)
|
||||||
|
|
||||||
|
# bob tries to spend money
|
||||||
|
outputs = {self.bob.getnewaddress() : 1.999}
|
||||||
|
bob_tx = self.bob.createrawtransaction(inputs=[], outputs=outputs, version=3)
|
||||||
|
|
||||||
|
assert_raises_rpc_error(
|
||||||
|
-4,
|
||||||
|
"Insufficient funds",
|
||||||
|
self.bob.fundrawtransaction,
|
||||||
|
bob_tx, {'include_unsafe': True}
|
||||||
|
)
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def truc_tx_with_conflicting_sibling_change(self):
|
||||||
|
self.log.info("Test v3 transaction with conflicting sibling change")
|
||||||
|
|
||||||
|
outputs = {self.alice.getnewaddress() : 8.0}
|
||||||
|
self.send_tx(self.charlie, [], outputs, 3)
|
||||||
|
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
|
||||||
|
# unconfirmed v3 tx to alice & bob
|
||||||
|
outputs = {self.alice.getnewaddress() : 2.0, self.bob.getnewaddress() : 2.0}
|
||||||
|
self.send_tx(self.alice, [], outputs, 3)
|
||||||
|
|
||||||
|
# bob spends his output with a v3 transaction
|
||||||
|
bob_unspent = self.bob.listunspent(minconf=0)[0]
|
||||||
|
outputs = {self.bob.getnewaddress() : bob_unspent['amount'] - Decimal(0.00000120)}
|
||||||
|
self.send_tx(self.bob, [bob_unspent], outputs, 3)
|
||||||
|
|
||||||
|
# alice tries to spend money
|
||||||
|
outputs = {self.alice.getnewaddress() : 1.999}
|
||||||
|
alice_tx = self.alice.createrawtransaction(inputs=[], outputs=outputs, version=3)
|
||||||
|
|
||||||
|
assert_raises_rpc_error(
|
||||||
|
-4,
|
||||||
|
"Insufficient funds",
|
||||||
|
self.alice.fundrawtransaction,
|
||||||
|
alice_tx, {'include_unsafe': True}
|
||||||
|
)
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def spend_inputs_with_different_versions(self, version_a, version_b):
|
||||||
|
self.log.info(f"Test spending a pre-selected v{version_a} input with a v{version_b} transaction")
|
||||||
|
|
||||||
|
outputs = {self.alice.getnewaddress() : 2.0}
|
||||||
|
self.send_tx(self.charlie, [], outputs, version_a)
|
||||||
|
|
||||||
|
# alice spends her output
|
||||||
|
alice_unspent = self.alice.listunspent(minconf=0)[0]
|
||||||
|
outputs = {self.alice.getnewaddress() : alice_unspent['amount'] - Decimal(0.00000120)}
|
||||||
|
alice_tx = self.alice.createrawtransaction(inputs=[alice_unspent], outputs=outputs, version=version_b)
|
||||||
|
|
||||||
|
assert_raises_rpc_error(
|
||||||
|
-4,
|
||||||
|
f"Can't spend unconfirmed version {version_a} pre-selected input with a version {version_b} tx",
|
||||||
|
self.alice.fundrawtransaction,
|
||||||
|
alice_tx
|
||||||
|
)
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def spend_inputs_with_different_versions_default_version(self):
|
||||||
|
self.log.info("Test spending a pre-selected v3 input with the default version of transaction")
|
||||||
|
|
||||||
|
outputs = {self.alice.getnewaddress() : 2.0}
|
||||||
|
self.send_tx(self.charlie, [], outputs, 3)
|
||||||
|
|
||||||
|
# alice spends her output
|
||||||
|
alice_unspent = self.alice.listunspent(minconf=0)[0]
|
||||||
|
outputs = {self.alice.getnewaddress() : alice_unspent['amount'] - Decimal(0.00000120)}
|
||||||
|
alice_tx = self.alice.createrawtransaction(inputs=[alice_unspent], outputs=outputs) # don't set the version here
|
||||||
|
|
||||||
|
assert_raises_rpc_error(
|
||||||
|
-4,
|
||||||
|
"Can't spend unconfirmed version 3 pre-selected input with a version 2 tx",
|
||||||
|
self.alice.fundrawtransaction,
|
||||||
|
alice_tx
|
||||||
|
)
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def v3_tx_evicted_from_mempool_by_sibling(self):
|
||||||
|
self.log.info("Test v3 transaction evicted because of conflicting sibling")
|
||||||
|
|
||||||
|
# unconfirmed v3 tx to alice & bob
|
||||||
|
outputs = {self.alice.getnewaddress() : 2.0, self.bob.getnewaddress() : 2.0}
|
||||||
|
self.send_tx(self.charlie, [], outputs, 3)
|
||||||
|
|
||||||
|
# alice spends her output with a v3 transaction
|
||||||
|
alice_unspent = self.alice.listunspent(minconf=0)[0]
|
||||||
|
alice_fee = Decimal(0.00000120)
|
||||||
|
outputs = {self.alice.getnewaddress() : alice_unspent['amount'] - alice_fee}
|
||||||
|
alice_txid = self.send_tx(self.alice, [alice_unspent], outputs, 3)
|
||||||
|
|
||||||
|
# bob tries to spend money
|
||||||
|
bob_unspent = self.bob.listunspent(minconf=0)[0]
|
||||||
|
outputs = {self.bob.getnewaddress() : bob_unspent['amount'] - Decimal(0.00010120)}
|
||||||
|
bob_txid = self.send_tx(self.bob, [bob_unspent], outputs, 3)
|
||||||
|
|
||||||
|
assert_equal(self.alice.gettransaction(alice_txid)['mempoolconflicts'], [bob_txid])
|
||||||
|
|
||||||
|
self.log.info("Test that re-submitting Alice's transaction with a higher fee removes bob's tx as a mempool conflict")
|
||||||
|
fee_delta = Decimal(0.00030120)
|
||||||
|
outputs = {self.alice.getnewaddress() : alice_unspent['amount'] - fee_delta}
|
||||||
|
alice_txid = self.send_tx(self.alice, [alice_unspent], outputs, 3)
|
||||||
|
assert_equal(self.alice.gettransaction(alice_txid)['mempoolconflicts'], [])
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def v3_conflict_removed_from_mempool(self):
|
||||||
|
self.log.info("Test a v3 conflict being removed")
|
||||||
|
# send a v2 output to alice and confirm it
|
||||||
|
txid = self.charlie.sendall([self.alice.getnewaddress()])["txid"]
|
||||||
|
assert_equal(self.charlie.gettransaction(txid, verbose=True)["decoded"]["version"], 2)
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
# create a v3 tx to alice and bob
|
||||||
|
outputs = {self.alice.getnewaddress() : 2.0, self.bob.getnewaddress() : 2.0}
|
||||||
|
self.send_tx(self.charlie, [], outputs, 3)
|
||||||
|
|
||||||
|
alice_v2_unspent = self.alice.listunspent(minconf=1)[0]
|
||||||
|
alice_unspent = self.alice.listunspent(minconf=0, maxconf=0)[0]
|
||||||
|
|
||||||
|
# alice spends both of her outputs
|
||||||
|
outputs = {self.charlie.getnewaddress() : alice_v2_unspent['amount'] + alice_unspent['amount'] - Decimal(0.00005120)}
|
||||||
|
self.send_tx(self.alice, [alice_v2_unspent, alice_unspent], outputs, 3)
|
||||||
|
# bob can't create a transaction
|
||||||
|
outputs = {self.bob.getnewaddress() : 1.999}
|
||||||
|
bob_tx = self.bob.createrawtransaction(inputs=[], outputs=outputs, version=3)
|
||||||
|
|
||||||
|
assert_raises_rpc_error(
|
||||||
|
-4,
|
||||||
|
"Insufficient funds",
|
||||||
|
self.bob.fundrawtransaction,
|
||||||
|
bob_tx, {'include_unsafe': True}
|
||||||
|
)
|
||||||
|
# alice fee-bumps her tx so it only spends the v2 utxo
|
||||||
|
outputs = {self.charlie.getnewaddress() : alice_v2_unspent['amount'] - Decimal(0.00015120)}
|
||||||
|
self.send_tx(self.alice, [alice_v2_unspent], outputs, 2)
|
||||||
|
# bob can now create a transaction
|
||||||
|
outputs = {self.bob.getnewaddress() : 1.999}
|
||||||
|
self.send_tx(self.bob, [], outputs, 3)
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def mempool_conflicts_removed_when_v3_conflict_removed(self):
|
||||||
|
self.log.info("Test that we remove v3 txs from mempool_conflicts correctly")
|
||||||
|
# send a v2 output to alice and confirm it
|
||||||
|
txid = self.charlie.sendall([self.alice.getnewaddress()])["txid"]
|
||||||
|
assert_equal(self.charlie.gettransaction(txid, verbose=True)["decoded"]["version"], 2)
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
# create a v3 tx to alice and bob
|
||||||
|
outputs = {self.alice.getnewaddress() : 2.0, self.bob.getnewaddress() : 2.0}
|
||||||
|
self.send_tx(self.charlie, [], outputs, 3)
|
||||||
|
|
||||||
|
alice_v2_unspent = self.alice.listunspent(minconf=1)[0]
|
||||||
|
alice_unspent = self.alice.listunspent(minconf=0, maxconf=0)[0]
|
||||||
|
# bob spends his utxo
|
||||||
|
inputs=[]
|
||||||
|
outputs = {self.bob.getnewaddress() : 1.999}
|
||||||
|
bob_txid = self.send_tx(self.bob, inputs, outputs, 3)
|
||||||
|
# alice spends both of her utxos, replacing bob's tx
|
||||||
|
outputs = {self.charlie.getnewaddress() : alice_v2_unspent['amount'] + alice_unspent['amount'] - Decimal(0.00005120)}
|
||||||
|
alice_txid = self.send_tx(self.alice, [alice_v2_unspent, alice_unspent], outputs, 3)
|
||||||
|
# bob's tx now has a mempool conflict
|
||||||
|
assert_equal(self.bob.gettransaction(bob_txid)['mempoolconflicts'], [alice_txid])
|
||||||
|
# alice fee-bumps her tx so it only spends the v2 utxo
|
||||||
|
outputs = {self.charlie.getnewaddress() : alice_v2_unspent['amount'] - Decimal(0.00015120)}
|
||||||
|
self.send_tx(self.alice, [alice_v2_unspent], outputs, 2)
|
||||||
|
# bob's tx now has non conflicts and can be rebroadcast
|
||||||
|
bob_tx = self.bob.gettransaction(bob_txid)
|
||||||
|
assert_equal(bob_tx['mempoolconflicts'], [])
|
||||||
|
self.bob.sendrawtransaction(bob_tx['hex'])
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def max_tx_weight(self):
|
||||||
|
self.log.info("Test max v3 transaction weight.")
|
||||||
|
|
||||||
|
tx = CTransaction()
|
||||||
|
tx.version = 3 # make this a truc tx
|
||||||
|
# increase tx weight almost to the max truc size
|
||||||
|
self.bulk_tx(tx, 5, TRUC_MAX_VSIZE - 100)
|
||||||
|
|
||||||
|
assert_raises_rpc_error(
|
||||||
|
-4,
|
||||||
|
"The inputs size exceeds the maximum weight. Please try sending a smaller amount or manually consolidating your wallet's UTXOs",
|
||||||
|
self.charlie.fundrawtransaction,
|
||||||
|
tx.serialize_with_witness().hex(),
|
||||||
|
{'include_unsafe' : True}
|
||||||
|
)
|
||||||
|
|
||||||
|
tx.version = 2
|
||||||
|
self.charlie.fundrawtransaction(tx.serialize_with_witness().hex())
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def max_tx_child_weight(self):
|
||||||
|
self.log.info("Test max v3 transaction child weight.")
|
||||||
|
|
||||||
|
outputs = {self.alice.getnewaddress() : 10}
|
||||||
|
self.send_tx(self.charlie, [], outputs, 3)
|
||||||
|
|
||||||
|
tx = CTransaction()
|
||||||
|
tx.version = 3
|
||||||
|
|
||||||
|
self.bulk_tx(tx, 5, TRUC_CHILD_MAX_VSIZE - 100)
|
||||||
|
|
||||||
|
assert_raises_rpc_error(
|
||||||
|
-4,
|
||||||
|
"The inputs size exceeds the maximum weight. Please try sending a smaller amount or manually consolidating your wallet's UTXOs",
|
||||||
|
self.alice.fundrawtransaction,
|
||||||
|
tx.serialize_with_witness().hex(),
|
||||||
|
{'include_unsafe' : True}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
self.alice.fundrawtransaction(tx.serialize_with_witness().hex())
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def user_input_weight_not_overwritten(self):
|
||||||
|
self.log.info("Test that the user-input tx weight is not overwritten by the truc maximum")
|
||||||
|
|
||||||
|
tx = CTransaction()
|
||||||
|
tx.version = 3
|
||||||
|
|
||||||
|
self.bulk_tx(tx, 5, int(TRUC_MAX_VSIZE/2))
|
||||||
|
|
||||||
|
assert_raises_rpc_error(
|
||||||
|
-4,
|
||||||
|
"Maximum transaction weight is less than transaction weight without inputs",
|
||||||
|
self.charlie.fundrawtransaction,
|
||||||
|
tx.serialize_with_witness().hex(),
|
||||||
|
{'include_unsafe' : True, 'max_tx_weight' : int(TRUC_MAX_VSIZE/2)}
|
||||||
|
)
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def user_input_weight_not_overwritten_v3_child(self):
|
||||||
|
self.log.info("Test that the user-input tx weight is not overwritten by the truc child maximum")
|
||||||
|
|
||||||
|
outputs = {self.alice.getnewaddress() : 10}
|
||||||
|
self.send_tx(self.charlie, [], outputs, 3)
|
||||||
|
|
||||||
|
tx = CTransaction()
|
||||||
|
tx.version = 3
|
||||||
|
|
||||||
|
self.bulk_tx(tx, 5, int(TRUC_CHILD_MAX_VSIZE/2))
|
||||||
|
|
||||||
|
assert_raises_rpc_error(
|
||||||
|
-4,
|
||||||
|
"Maximum transaction weight is less than transaction weight without inputs",
|
||||||
|
self.alice.fundrawtransaction,
|
||||||
|
tx.serialize_with_witness().hex(),
|
||||||
|
{'include_unsafe' : True, 'max_tx_weight' : int(TRUC_CHILD_MAX_VSIZE/2)}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
self.alice.fundrawtransaction(tx.serialize_with_witness().hex())
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def createpsbt_v3(self):
|
||||||
|
self.log.info("Test setting version to 3 with createpsbt")
|
||||||
|
|
||||||
|
outputs = {self.alice.getnewaddress() : 10}
|
||||||
|
psbt = self.charlie.createpsbt(inputs=[], outputs=outputs, version=3)
|
||||||
|
assert_equal(self.charlie.decodepsbt(psbt)["tx"]["version"], 3)
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def send_v3(self):
|
||||||
|
self.log.info("Test setting version to 3 with send")
|
||||||
|
|
||||||
|
outputs = {self.alice.getnewaddress() : 10}
|
||||||
|
tx_hex = self.charlie.send(outputs=outputs, add_to_wallet=False, version=3)["hex"]
|
||||||
|
assert_equal(self.charlie.decoderawtransaction(tx_hex)["version"], 3)
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def sendall_v3(self):
|
||||||
|
self.log.info("Test setting version to 3 with sendall")
|
||||||
|
|
||||||
|
tx_hex = self.charlie.sendall(recipients=[self.alice.getnewaddress()], version=3, add_to_wallet=False)["hex"]
|
||||||
|
assert_equal(self.charlie.decoderawtransaction(tx_hex)["version"], 3)
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def sendall_with_unconfirmed_v3(self):
|
||||||
|
self.log.info("Test setting version to 3 with sendall + unconfirmed inputs")
|
||||||
|
|
||||||
|
outputs = {self.alice.getnewaddress(): 2.00001 for _ in range(4)}
|
||||||
|
|
||||||
|
self.send_tx(self.charlie, [], outputs, 2)
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
|
||||||
|
unspents = self.alice.listunspent()
|
||||||
|
|
||||||
|
# confirmed v2 utxos
|
||||||
|
outputs = {self.alice.getnewaddress() : 2.0}
|
||||||
|
confirmed_v2 = self.send_tx(self.alice, [unspents[0]], outputs, 2)
|
||||||
|
|
||||||
|
# confirmed v3 utxos
|
||||||
|
outputs = {self.alice.getnewaddress() : 2.0}
|
||||||
|
confirmed_v3 = self.send_tx(self.alice, [unspents[1]], outputs, 3)
|
||||||
|
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
|
||||||
|
# unconfirmed v2 utxos
|
||||||
|
outputs = {self.alice.getnewaddress() : 2.0}
|
||||||
|
unconfirmed_v2 = self.send_tx(self.alice, [unspents[2]], outputs, 2)
|
||||||
|
|
||||||
|
# unconfirmed v3 utxos
|
||||||
|
outputs = {self.alice.getnewaddress() : 2.0}
|
||||||
|
unconfirmed_v3 = self.send_tx(self.alice, [unspents[3]], outputs, 3)
|
||||||
|
|
||||||
|
# Test that the only unconfirmed inputs this v3 tx spends are v3
|
||||||
|
tx_hex = self.alice.sendall([self.bob.getnewaddress()], version=3, add_to_wallet=False, minconf=0)["hex"]
|
||||||
|
|
||||||
|
decoded_tx = self.alice.decoderawtransaction(tx_hex)
|
||||||
|
decoded_vin_txids = [txin["txid"] for txin in decoded_tx["vin"]]
|
||||||
|
|
||||||
|
assert_equal(decoded_tx["version"], 3)
|
||||||
|
|
||||||
|
assert confirmed_v3 in decoded_vin_txids
|
||||||
|
assert confirmed_v2 in decoded_vin_txids
|
||||||
|
assert unconfirmed_v3 in decoded_vin_txids
|
||||||
|
assert unconfirmed_v2 not in decoded_vin_txids
|
||||||
|
|
||||||
|
# Test that the only unconfirmed inputs this v2 tx spends are v2
|
||||||
|
tx_hex = self.alice.sendall([self.bob.getnewaddress()], version=2, add_to_wallet=False, minconf=0)["hex"]
|
||||||
|
|
||||||
|
decoded_tx = self.alice.decoderawtransaction(tx_hex)
|
||||||
|
decoded_vin_txids = [txin["txid"] for txin in decoded_tx["vin"]]
|
||||||
|
|
||||||
|
assert_equal(decoded_tx["version"], 2)
|
||||||
|
|
||||||
|
assert confirmed_v3 in decoded_vin_txids
|
||||||
|
assert confirmed_v2 in decoded_vin_txids
|
||||||
|
assert unconfirmed_v2 in decoded_vin_txids
|
||||||
|
assert unconfirmed_v3 not in decoded_vin_txids
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def walletcreatefundedpsbt_v3(self):
|
||||||
|
self.log.info("Test setting version to 3 with walletcreatefundedpsbt")
|
||||||
|
|
||||||
|
outputs = {self.alice.getnewaddress() : 10}
|
||||||
|
psbt = self.charlie.walletcreatefundedpsbt(inputs=[], outputs=outputs, version=3)["psbt"]
|
||||||
|
assert_equal(self.charlie.decodepsbt(psbt)["tx"]["version"], 3)
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def sendall_truc_weight_limit(self):
|
||||||
|
self.log.info("Test that sendall follows truc tx weight limit")
|
||||||
|
self.charlie.sendall([self.alice.getnewaddress() for _ in range(300)], add_to_wallet=False, version=2)
|
||||||
|
|
||||||
|
# check that error is only raised if version is 3
|
||||||
|
assert_raises_rpc_error(
|
||||||
|
-4,
|
||||||
|
"Transaction too large" ,
|
||||||
|
self.charlie.sendall,
|
||||||
|
[self.alice.getnewaddress() for _ in range(300)],
|
||||||
|
version=3
|
||||||
|
)
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def sendall_truc_child_weight_limit(self):
|
||||||
|
self.log.info("Test that sendall follows spending unconfirmed truc tx weight limit")
|
||||||
|
outputs = {self.charlie.getnewaddress() : 2.0}
|
||||||
|
self.send_tx(self.charlie, [], outputs, 3)
|
||||||
|
|
||||||
|
self.charlie.sendall([self.alice.getnewaddress() for _ in range(50)], add_to_wallet=False)
|
||||||
|
|
||||||
|
assert_raises_rpc_error(
|
||||||
|
-4,
|
||||||
|
"Transaction too large" ,
|
||||||
|
self.charlie.sendall,
|
||||||
|
[self.alice.getnewaddress() for _ in range(50)],
|
||||||
|
version=3
|
||||||
|
)
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def mix_non_truc_versions(self):
|
||||||
|
self.log.info("Test that we can mix non-truc versions when spending an unconfirmed output")
|
||||||
|
|
||||||
|
outputs = {self.bob.getnewaddress() : 2.0}
|
||||||
|
self.send_tx(self.charlie, [], outputs, 1)
|
||||||
|
|
||||||
|
assert_equal(self.bob.getbalances()["mine"]["trusted"], 0)
|
||||||
|
assert_greater_than(self.bob.getbalances()["mine"]["untrusted_pending"], 0)
|
||||||
|
|
||||||
|
outputs = {self.alice.getnewaddress() : 1.0}
|
||||||
|
|
||||||
|
raw_tx_v2 = self.bob.createrawtransaction(inputs=[], outputs=outputs, version=2)
|
||||||
|
|
||||||
|
# does not throw an error
|
||||||
|
self.bob.fundrawtransaction(raw_tx_v2, {'include_unsafe': True})["hex"]
|
||||||
|
|
||||||
|
@cleanup
|
||||||
|
def cant_spend_multiple_unconfirmed_truc_outputs(self):
|
||||||
|
self.log.info("Test that we can't spend multiple unconfirmed truc outputs")
|
||||||
|
|
||||||
|
outputs = {self.alice.getnewaddress(): 2.00001}
|
||||||
|
self.send_tx(self.charlie, [], outputs, 3)
|
||||||
|
self.send_tx(self.charlie, [], outputs, 3)
|
||||||
|
|
||||||
|
assert_equal(len(self.alice.listunspent(minconf=0)), 2)
|
||||||
|
|
||||||
|
outputs = {self.bob.getnewaddress() : 3.0}
|
||||||
|
|
||||||
|
raw_tx = self.alice.createrawtransaction(inputs=[], outputs=outputs, version=3)
|
||||||
|
|
||||||
|
assert_raises_rpc_error(
|
||||||
|
-4,
|
||||||
|
"Insufficient funds",
|
||||||
|
self.alice.fundrawtransaction,
|
||||||
|
raw_tx,
|
||||||
|
{'include_unsafe' : True}
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
WalletV3Test(__file__).main()
|
||||||
Reference in new issue
Block a user