Merge bitcoin/bitcoin#32896: wallet, rpc: add v3 transaction creation and wallet support

5c8bf7b39e doc: add release notes for version 3 transactions (ishaanam)
4ef8065a5e test: add truc wallet tests (ishaanam)
5d932e14db test: extract `bulk_vout` from `bulk_tx` so it can be used by wallet tests (ishaanam)
2cb473d9f2 rpc: Support version 3 transaction creation (Bue-von-hon)
4c20343b4d rpc: Add transaction min standard version parameter (Bue-von-hon)
c5a2d08011 wallet: don't return utxos from multiple truc txs in AvailableCoins (ishaanam)
da8748ad62 wallet: limit v3 tx weight in coin selection (ishaanam)
85c5410615 wallet: mark unconfirmed v3 siblings as mempool conflicts (ishaanam)
0804fc3cb1 wallet: throw error at conflicting tx versions in pre-selected inputs (ishaanam)
cc155226fe wallet: set m_version in coin control to default value (ishaanam)
2e9617664e  wallet: don't include unconfirmed v3 txs with children in available coins (ishaanam)
ec2676becd wallet: 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:
    reACK 5c8bf7b39e
  achow101:
    ACK 5c8bf7b39e
  rkrux:
    ACK 5c8bf7b39e

Tree-SHA512: da8aea51c113e193dd0b442eff765bd6b8dc0e5066272d3e52190a223c903f48788795f32c554f268af0d2607b5b8c3985c648879cb176c65540837c05d0abb5
This commit is contained in:
merge-script
2025-08-19 06:00:50 -04:00
25 changed files with 836 additions and 32 deletions

View File

@@ -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)
{
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";
return false;
}

View File

@@ -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
// adapting relay policy by bumping TX_MAX_STANDARD_VERSION, and then later
// 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};
/**

View File

@@ -28,8 +28,10 @@ static constexpr unsigned int TRUC_ANCESTOR_LIMIT{2};
/** Maximum sigop-adjusted virtual size of all v3 transactions. */
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. */
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.
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);

View File

@@ -119,6 +119,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "createrawtransaction", 1, "outputs" },
{ "createrawtransaction", 2, "locktime" },
{ "createrawtransaction", 3, "replaceable" },
{ "createrawtransaction", 4, "version" },
{ "decoderawtransaction", 1, "iswitness" },
{ "signrawtransactionwithkey", 1, "privkeys" },
{ "signrawtransactionwithkey", 2, "prevtxs" },
@@ -167,6 +168,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "walletcreatefundedpsbt", 3, "solving_data"},
{ "walletcreatefundedpsbt", 3, "max_tx_weight"},
{ "walletcreatefundedpsbt", 4, "bip32derivs" },
{ "walletcreatefundedpsbt", 5, "version" },
{ "walletprocesspsbt", 1, "sign" },
{ "walletprocesspsbt", 3, "bip32derivs" },
{ "walletprocesspsbt", 4, "finalize" },
@@ -177,6 +179,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "createpsbt", 1, "outputs" },
{ "createpsbt", 2, "locktime" },
{ "createpsbt", 3, "replaceable" },
{ "createpsbt", 4, "version" },
{ "combinepsbt", 0, "txs"},
{ "joinpsbts", 0, "txs"},
{ "finalizepsbt", 1, "extract"},
@@ -213,6 +216,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "send", 4, "replaceable"},
{ "send", 4, "solving_data"},
{ "send", 4, "max_tx_weight"},
{ "send", 5, "version"},
{ "sendall", 0, "recipients" },
{ "sendall", 1, "conf_target" },
{ "sendall", 3, "fee_rate"},
@@ -230,6 +234,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "sendall", 4, "conf_target"},
{ "sendall", 4, "replaceable"},
{ "sendall", 4, "solving_data"},
{ "sendall", 4, "version"},
{ "simulaterawtransaction", 0, "rawtxs" },
{ "simulaterawtransaction", 1, "options" },
{ "simulaterawtransaction", 1, "include_watchonly"},

View File

@@ -53,6 +53,8 @@ using node::GetTransaction;
using node::NodeContext;
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,
Chainstate& active_chainstate, const CTxUndo* txundo = nullptr,
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"},
{"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."},
{"version", RPCArg::Type::NUM, RPCArg::Default{DEFAULT_RAWTX_VERSION}, "Transaction version"},
};
}
@@ -437,7 +440,7 @@ static RPCHelpMan createrawtransaction()
if (!request.params[3].isNull()) {
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));
},
@@ -1679,7 +1682,7 @@ static RPCHelpMan createpsbt()
if (!request.params[3].isNull()) {
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
PartiallySignedTransaction psbtx;

View File

@@ -18,6 +18,7 @@
#include <tinyformat.h>
#include <univalue.h>
#include <util/rbf.h>
#include <util/string.h>
#include <util/strencodings.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;
@@ -154,6 +155,11 @@ CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniVal
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);
AddOutputs(rawTx, outputs_in);

View File

@@ -53,6 +53,6 @@ std::vector<std::pair<CTxDestination, CAmount>> ParseOutputs(const UniValue& out
void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in);
/** 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

View File

@@ -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, int, CHECK_NONFATAL(maybe_arg)->getInt<int>(););
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(););
bool RPCHelpMan::IsValidNumArgs(size_t num_args) const

View File

@@ -21,6 +21,8 @@ namespace wallet {
const int DEFAULT_MIN_DEPTH = 0;
const int DEFAULT_MAX_DEPTH = 9999999;
const int DEFAULT_WALLET_TX_VERSION = CTransaction::CURRENT_VERSION;
//! Default for -avoidpartialspends
static constexpr bool DEFAULT_AVOIDPARTIALSPENDS = false;
@@ -109,10 +111,10 @@ public:
int m_max_depth = DEFAULT_MAX_DEPTH;
//! SigningProvider that has pubkeys and scripts to do spend size estimation for external inputs
FlatSigningProvider m_external_provider;
//! Version
uint32_t m_version = DEFAULT_WALLET_TX_VERSION;
//! Locktime
std::optional<uint32_t> m_locktime;
//! Version
std::optional<uint32_t> m_version;
//! Caps weight of resulting tx
std::optional<int> m_max_tx_weight{std::nullopt};

View File

@@ -174,6 +174,8 @@ struct CoinSelectionParams {
* 1) Received from other wallets, 2) replacing other txs, 3) that have been replaced.
*/
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. */
std::optional<int> m_max_tx_weight{std::nullopt};

View File

@@ -8,6 +8,7 @@
#include <key_io.h>
#include <node/types.h>
#include <policy/policy.h>
#include <policy/truc_policy.h>
#include <rpc/rawtransaction_util.h>
#include <rpc/util.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>();
}
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())
throw JSONRPCError(RPC_INVALID_PARAMETER, "TX must have at least one output");
@@ -1269,6 +1276,7 @@ RPCHelpMan send()
},
FundTxDoc()),
RPCArgOptions{.oneline_description="options"}},
{"version", RPCArg::Type::NUM, RPCArg::Default{DEFAULT_WALLET_TX_VERSION}, "Transaction version"},
},
RPCResult{
RPCResult::Type::OBJ, "", "",
@@ -1308,14 +1316,16 @@ RPCHelpMan send()
ParseOutputs(outputs),
InterpretSubtractFeeFromOutputInstructions(options["subtract_fee_from_outputs"], outputs.getKeys())
);
CMutableTransaction rawTx = ConstructTransaction(options["inputs"], request.params[0], options["locktime"], rbf);
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
// be overridden by options.add_inputs.
coin_control.m_allow_other_inputs = rawTx.vin.size() == 0;
if (options.exists("max_tx_weight")) {
coin_control.m_max_tx_weight = options["max_tx_weight"].getInt<int>();
}
SetOptionsInputWeights(options["inputs"], options);
// 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
@@ -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."},
{"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."},
{"version", RPCArg::Type::NUM, RPCArg::Default{DEFAULT_WALLET_TX_VERSION}, "Transaction version"},
},
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};
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.");
}
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);
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)) {
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;
}
} else {
@@ -1497,6 +1525,10 @@ RPCHelpMan sendall()
if (send_max && fee_rate.GetFee(output.input_bytes) > output.txout.nValue) {
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);
rawTx.vin.push_back(input);
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 (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.");
}
@@ -1731,6 +1763,7 @@ RPCHelpMan walletcreatefundedpsbt()
FundTxDoc()),
RPCArgOptions{.oneline_description="options"}},
{"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::Type::OBJ, "", "",
@@ -1758,16 +1791,18 @@ RPCHelpMan walletcreatefundedpsbt()
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 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);
outputs = NormalizeOutputs(request.params[1]);
std::vector<CRecipient> recipients = CreateRecipients(
ParseOutputs(outputs),
InterpretSubtractFeeFromOutputInstructions(options["subtractFeeFromOutputs"], outputs.getKeys())
);
CCoinControl coin_control;
// Automatically select coins, unless at least one is manually selected. Can
// be overridden by options.add_inputs.
coin_control.m_allow_other_inputs = rawTx.vin.size() == 0;

View File

@@ -12,6 +12,7 @@
#include <node/types.h>
#include <numeric>
#include <policy/policy.h>
#include <policy/truc_policy.h>
#include <primitives/transaction.h>
#include <primitives/transaction_identifier.h>
#include <script/script.h>
@@ -282,6 +283,14 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const
if (input_bytes == -1) {
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 {
// The input is external. We did not find the tx in mapWallet.
const auto out{coin_control.GetExternalOutput(outpoint)};
@@ -316,6 +325,9 @@ CoinsResult AvailableCoins(const CWallet& wallet,
AssertLockHeld(wallet.cs_wallet);
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
// 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);
@@ -386,6 +398,17 @@ CoinsResult AvailableCoins(const CWallet& wallet,
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) {
continue;
}
@@ -450,8 +473,15 @@ CoinsResult AvailableCoins(const CWallet& wallet,
is_from_p2sh = true;
}
result.Add(GetOutputType(type, is_from_p2sh),
COutput(outpoint, output, nDepth, input_bytes, spendable, solvable, tx_safe, wtx.GetTxTime(), tx_from_me, feerate));
auto available_output_type = GetOutputType(type, is_from_p2sh);
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);
@@ -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()) {
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)
{
params.only_spendable = false;
params.check_version_trucness = false;
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).
// future: add "error level" so the worst one can be picked instead.
std::vector<util::Result<SelectionResult>> res_detailed_errors;
CoinSelectionParams updated_selection_params = coin_selection_params;
for (const auto& select_filter : ordered_filters) {
auto it = filtered_groups.find(select_filter.filter);
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,
coin_selection_params, select_filter.allow_mixed_output_types)}) {
updated_selection_params, select_filter.allow_mixed_output_types)}) {
return res; // result found
} else {
// 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;
CMutableTransaction txNew; // The resulting transaction that we make
if (coin_control.m_version) {
txNew.version = coin_control.m_version.value();
}
txNew.version = coin_control.m_version;
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_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_version = coin_control.m_version;
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) {
return util::Error{strprintf(_("Maximum transaction weight must be between %d and %d"), minimum_tx_weight, MAX_STANDARD_TX_WEIGHT)};

View File

@@ -83,6 +83,9 @@ struct CoinFilterParams {
bool include_immature_coinbase{false};
// By default, skip locked UTXOs
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};
};
/**

View File

@@ -258,6 +258,10 @@ public:
// BlockConflicted.
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>
void Serialize(Stream& s) const
{

View File

@@ -30,6 +30,7 @@
#include <node/types.h>
#include <outputtype.h>
#include <policy/feerate.h>
#include <policy/truc_policy.h>
#include <primitives/block.h>
#include <primitives/transaction.h>
#include <psbt.h>
@@ -1213,6 +1214,23 @@ bool CWallet::TransactionCanBeAbandoned(const Txid& hashTx) const
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)
{
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;
});
}
}
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)

View File

@@ -450,6 +450,9 @@ private:
// Update last block processed in memory only
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:
/**
* Main wallet lock.