wallet: don’t consider unconfirmed TRUC coins with ancestors #33528

pull glozow wants to merge 2 commits into bitcoin:master from glozow:2025-09-send changing 2 files +50 −0
  1. glozow commented at 10:10 pm on October 2, 2025: member

    Addresses #33368 (comment)

    There is not an explicit check that the to-be-created wallet transaction would be within the {TRUC, normal} ancestor limits. This means that the wallet may create a transaction that violates these limits, but fail to broadcast it in CommitTransaction.

    This appears to be expected behavior for the normal ancestor limits (and any other situation in which the wallet creates a tx that was rejected by mempool) and AFAIK the transaction will be rebroadcast at some point after the ancestors confirm.

    https://github.com/bitcoin/bitcoin/blob/1ed00a0d39d5190d8ad88a0dd705a09b56d987aa/test/functional/wallet_basic.py#L502-L506

    It’s a bit complex to address this for the normal ancestor limit, and probably unrealistic for the wallet to check all possible mempool policies in coin selection, but it’s quite trivial for TRUC: just skip any unconfirmed UTXOs that have any ancestors. I think it would be much more helpful to the user to say there are insufficient funds.

  2. [wallet] never try to spend from unconfirmed TRUC that already has ancestors e753fadfd0
  3. [test] wallet send 3 generation TRUC dcd42d6d8f
  4. glozow added the label Wallet on Oct 2, 2025
  5. DrahtBot commented at 10:10 pm on October 2, 2025: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/33528.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK achow101, rkrux, monlovesmango

    If your review is incorrectly listed, please copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

  6. fanquake commented at 3:55 pm on October 3, 2025: member
    Is this going to be backported to 30.x?
  7. glozow commented at 5:04 pm on October 3, 2025: member

    Is this going to be backported to 30.x?

    I think that’d be best so that the behavior is consistent from v30 onwards. But I don’t think this should hold up the release.

  8. fanquake added the label Needs backport (30.x) on Oct 3, 2025
  9. monlovesmango commented at 5:21 pm on October 7, 2025: contributor

    Wow so on top of it @glozow :)

    A couple things.

    When I went back to test the commands I pasted in #33368 (comment) I found that it was still failing silently despite the functional test failing appropriately. I was able to tweak the functional test a bit and get it to also mimic the silent failing behavior in this branch: https://github.com/monlovesmango/bitcoin/tree/pr33528-functional-test-tweak I am still trying to figure out the root cause for why it no longer throws an exception when using send instead of sendall for the second tx, but wanted report back since you might have a much better idea of what is going on.

    The other thing is that another scenario I had identified and was waiting report to see if first scenario was even an issue was when tx1 has more than one output and wallet tries to send more than one child there is also a silent failure. Seems like this could be fixed by checking that parent doesn’t already have a child already? Below is the commands for testing this scenario based on v30 Testing Guide setup.

    0TRUC_address1=$(bcli30 getnewaddress)
    1TRUC_address2=$(bcli30 getnewaddress)
    2tx=$(bcli30 -named send outputs='{"'$TRUC_address1'": 1, "'$TRUC_address2'": 2}' fee_rate=25 version=3)
    3TRUC_address3=$(bcli30 getnewaddress)
    4TRUC_address4=$(bcli30 getnewaddress)
    5tx2=$(bcli30 -named send outputs='{"'$TRUC_address3'": 0.5}' options='{"inputs":[{"txid":"'$(echo $tx|jq -r ".txid")'","vout":0}]}' fee_rate=25 version=3)
    6tx3=$(bcli30 -named send outputs='{"'$TRUC_address4'": 0.5}' options='{"inputs":[{"txid":"'$(echo $tx|jq -r ".txid")'","vout":1}]}' fee_rate=25 version=3)
    7bcli30 getrawtransaction  $(echo $tx2|jq -r ".txid") 1
    8bcli30 getrawtransaction  $(echo $tx3|jq -r ".txid") 1
    
  10. fanquake added this to the milestone 30.1 on Oct 8, 2025
  11. glozow commented at 6:00 pm on October 8, 2025: member

    When I went back to test the commands I pasted in #33368 (comment) I found that it was still failing silently despite the functional test failing appropriately.

    Nice! This PR is limiting the UTXOs that show up when doing automatic coin selection, and thus errors with “insufficient funds” when it can’t find enough UTXOs for the tx. In your test/commands, you’re pre-selecting those inputs using send.

    Btw, when I was testing send commands on regtest, I found that sometimes I accidentally succeeded because the wallet had other confirmed UTXOs to spend. You can sanity check that you’re indeed failing silently using with self.nodes[0].assert_debug_log(["Transaction cannot be broadcast immediately"]).

    I think it would be reasonable to stop this transaction from being created and throw a “invalid parameters” error, telling the user they selected inputs that can’t be spent due to policy. Similar to here:

    https://github.com/bitcoin/bitcoin/blob/b510893d00760083ac36948747aa6ebd84656192/src/wallet/rpc/spend.cpp#L1511-L1516

  12. achow101 commented at 10:39 pm on November 19, 2025: member
    ACK dcd42d6d8f160ae8bc12c152099a6e6473658e30
  13. in src/wallet/spend.cpp:409 in dcd42d6d8f
    402@@ -403,6 +403,11 @@ CoinsResult AvailableCoins(const CWallet& wallet,
    403                     if (wtx.tx->version != TRUC_VERSION) continue;
    404                     // this unconfirmed v3 transaction already has a child
    405                     if (wtx.truc_child_in_mempool.has_value()) continue;
    406+
    407+                    // this unconfirmed v3 transaction has a parent: spending would create a third generation
    408+                    size_t ancestors, descendants;
    409+                    wallet.chain().getTransactionAncestry(wtx.tx->GetHash(), ancestors, descendants);
    


    rkrux commented at 1:10 pm on November 24, 2025:

    There appears to be two different ways now to check for the mempool ancestors and descendants of the transaction here. I tried the following diff and the tests pass. Maybe we can remove the truc_child_in_mempool class member altogether in the future?

     0diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp
     1index 5654c8f3d4..65c896892f 100644
     2--- a/src/wallet/spend.cpp
     3+++ b/src/wallet/spend.cpp
     4@@ -401,16 +401,15 @@ CoinsResult AvailableCoins(const CWallet& wallet,
     5             if (nDepth == 0 && params.check_version_trucness) {
     6                 if (coinControl->m_version == TRUC_VERSION) {
     7                     if (wtx.tx->version != TRUC_VERSION) continue;
     8+                    size_t ancestors, descendants;
     9+                    wallet.chain().getTransactionAncestry(wtx.tx->GetHash(), ancestors, descendants);
    10                     // this unconfirmed v3 transaction already has a child
    11-                    if (wtx.truc_child_in_mempool.has_value()) continue;
    12+                    if (descendants > 1) continue;
    13 
    14                     // this unconfirmed v3 transaction has a parent: spending would create a third generation
    15-                    size_t ancestors, descendants;
    16-                    wallet.chain().getTransactionAncestry(wtx.tx->GetHash(), ancestors, descendants);
    17                     if (ancestors > 1) continue;
    18                 } else {
    19                     if (wtx.tx->version == TRUC_VERSION) continue;
    20-                    Assume(!wtx.truc_child_in_mempool.has_value());
    21                 }
    22             }
    23 
    24diff --git a/src/wallet/transaction.h b/src/wallet/transaction.h
    25index 1dbcdd2d92..7c97bb7f1a 100644
    26--- a/src/wallet/transaction.h
    27+++ b/src/wallet/transaction.h
    28@@ -277,10 +277,6 @@ public:
    29     // BlockConflicted.
    30     std::set<Txid> mempool_conflicts;
    31 
    32-    // Track v3 mempool tx that spends from this tx
    33-    // so that we don't try to create another unconfirmed child
    34-    std::optional<Txid> truc_child_in_mempool;
    35-
    36     template<typename Stream>
    37     void Serialize(Stream& s) const
    38     {
    39diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
    40index d08d6782c1..ea1d78b310 100644
    41--- a/src/wallet/wallet.cpp
    42+++ b/src/wallet/wallet.cpp
    43@@ -1398,7 +1398,6 @@ void CWallet::transactionAddedToMempool(const CTransactionRef& tx) {
    44             if (parent_it != mapWallet.end()) {
    45                 CWalletTx& parent_wtx = parent_it->second;
    46                 if (parent_wtx.isUnconfirmed()) {
    47-                    parent_wtx.truc_child_in_mempool = tx->GetHash();
    48                     // Even though these siblings do not spend the same utxos, they can't
    49                     // be present in the mempool at the same time because of TRUC policy rules
    50                     UpdateTrucSiblingConflicts(parent_wtx, txid, /*add_conflict=*/true);
    51@@ -1460,16 +1459,11 @@ void CWallet::transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRe
    52     }
    53 
    54     if (tx->version == TRUC_VERSION) {
    55-        // If this tx has a parent, unset its truc_child_in_mempool to make it possible
    56-        // to spend from the parent again. If this tx was replaced by another
    57-        // child of the same parent, transactionAddedToMempool
    58-        // will update truc_child_in_mempool
    59         for (const CTxIn& tx_in : tx->vin) {
    60             auto parent_it = mapWallet.find(tx_in.prevout.hash);
    61             if (parent_it != mapWallet.end()) {
    62                 CWalletTx& parent_wtx = parent_it->second;
    63-                if (parent_wtx.truc_child_in_mempool == tx->GetHash()) {
    64-                    parent_wtx.truc_child_in_mempool = std::nullopt;
    65+                if (parent_wtx.isUnconfirmed()) {
    66                     UpdateTrucSiblingConflicts(parent_wtx, txid, /*add_conflict=*/false);
    67                 }
    68             }
    

    monlovesmango commented at 6:24 pm on November 24, 2025:
    As far as I can tell descendants from getTransactionAncestry actually finds the parent(/ancestor) with the most descendants and returns that count, not the count of children for txid in question.

    rkrux commented at 10:36 am on November 25, 2025:

    I see, you might be correct. I hadn’t checked the function implementation and assumed this based on the function doc that gave this impression to me.

    https://github.com/bitcoin/bitcoin/blob/238c1c8933b1f7479a9bca2b7cb207d26151c39d/src/interfaces/chain.h#L224-L225

    https://github.com/bitcoin/bitcoin/blob/238c1c8933b1f7479a9bca2b7cb207d26151c39d/src/txmempool.h#L592-L598

  14. rkrux commented at 1:30 pm on November 24, 2025: contributor

    lgtm ACK dcd42d6d8f160ae8bc12c152099a6e6473658e30

    Asked a question.

  15. monlovesmango commented at 4:36 pm on November 24, 2025: contributor

    ACK dcd42d6d8f160ae8bc12c152099a6e6473658e30

    It makes sense to ensure selected coins do not have more than 1 unconfirmed ancestor to prevent selecting coins that would create a package with size greater than 2.

  16. monlovesmango commented at 11:38 pm on November 24, 2025: contributor

    I think it would be reasonable to stop this transaction from being created and throw a “invalid parameters” error, telling the user they selected inputs that can’t be spent due to policy. @glozow I created 2 commits that build off of this pr’s branch in an attempt to address this here: https://github.com/monlovesmango/bitcoin/tree/pr33528-add-invalid-parameters-error

    Any feedback is greatly appreciated. Feel free to take anything you think is useful. If you don’t think its appropriate for this PR do you think opening a separate PR be ok?

    The changes in this branch will address #33368 (comment) (this PR as is technically doesn’t address this) and #33528 (comment).

  17. fanquake closed this on Dec 3, 2025

  18. fanquake reopened this on Dec 3, 2025

  19. fanquake commented at 11:40 am on December 3, 2025: member

    Locally I could not run the merge script against this PR:

    0fatal: ambiguous argument 'refs/heads/pull/33528/merge': unknown revision or path not in the working tree.
    1Use '--' to separate paths from revisions, like this:
    2'git <command> [<revision>...] -- [<file>...]'
    3ERROR: Cannot find merge of pull request bitcoin/bitcoin#33528 on git@github.com:bitcoin/bitcoin.
    

    Open/closed to re-run CI, and it seems to have the same issue.

  20. DrahtBot added the label CI failed on Dec 3, 2025
  21. DrahtBot removed the label CI failed on Dec 3, 2025
  22. maflcko commented at 7:58 pm on December 3, 2025: member

    Open/closed to re-run CI, and it seems to have the same issue.

    Is it fixed now?

  23. fanquake merged this on Dec 4, 2025
  24. fanquake closed this on Dec 4, 2025

  25. fanquake removed the label Needs backport (30.x) on Dec 4, 2025
  26. fanquake commented at 10:03 am on December 4, 2025: member
    Backported to 30.x in #33997.
  27. fanquake referenced this in commit 52cd520497 on Dec 4, 2025
  28. fanquake referenced this in commit ab58b2c0f8 on Dec 5, 2025
  29. fanquake referenced this in commit 187e3b89b5 on Dec 5, 2025
  30. fanquake referenced this in commit b1499ddf8b on Dec 5, 2025
  31. glozow deleted the branch on Dec 11, 2025

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-12-13 03:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me