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

@@ -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)