refactor: TxDownloadManager + fuzzing #30110

pull glozow wants to merge 29 commits into bitcoin:master from glozow:2024-05-txdownload changing 11 files +2272 −546
  1. glozow commented at 9:49 am on May 15, 2024: member

    Part of #27463.

    This PR does 2 things:

    (1) It modularizes transaction download logic into a TxDownloadManager. Transaction download logic refers to the process of deciding what transactions to request, download, and validate.[1] There should be no behavior changes. Using --color_moved=dimmed_zebra -w may help. (2) It adds unit and fuzz (:magic_wand:) testing for transaction download.

    There are several benefits to this interface, such as:

    • Unit test coverage and fuzzing for logic that currently isn’t feasible to test as thoroughly (without lots of overhead) and/or currently only lightly tested through assert_debug_log (not good) in functional tests.
    • When we add more functionality (e.g. package relay messages, more robust orphan handling), the vast majority of it will be within TxDownloadManager instead of PeerManager, making it easier to review and test. See #28031 for what this looks like.
    • PeerManager will no longer know anything about / have access to TxOrphanage, TxRequestTracker or the rejection caches. Its primary interface with TxDownloadManager would be much simpler:
      • Passing on ValidationInterface callbacks
      • Telling txdownloadman when a peer {connects, disconnects}
      • Telling txdownloadmanwhen a {transaction, package} is {accepted, rejected} from mempool
      • Telling txdownloadman when invs, notfounds, and txs are received.
      • Getting instructions on what to download.
      • Getting instructions on what {transactions, packages, orphans} to validate.
      • Get whether a peer HaveMoreWork for the ProessMessages loop
    • (todo) Thread-safety can be handled internally.

    [1]: This module is concerned with tx download, not upload. It excludes transaction announcements/gossip which happens after we download/accept a transaction. Txreconciliation (erlay) is excluded from this module, as it only relates to deciding which invs to send or helping the other peer decide which invs to send. It is independent from this logic.

  2. glozow added the label Refactoring on May 15, 2024
  3. DrahtBot commented at 9:49 am on May 15, 2024: contributor

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

    Code Coverage

    For detailed information about the code coverage, see the test coverage report.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK l0rinc
    Approach ACK ismaelsadeeq
    Stale ACK theStack, instagibbs

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #30572 (Halt processing of unrequested transactions v2 by ariard)
    • #30538 (Doc: add a comment referencing past vulnerability next to where it was fixed by darosior)
    • #30116 (p2p: Fill reconciliation sets (Erlay) attempt 2 by sr-gi)
    • #29492 (refactor: Remove redundant definitions by Empact)
    • #27052 (test: rpc: add last block announcement time to getpeerinfo result by LarryRuane)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  4. glozow force-pushed on May 15, 2024
  5. DrahtBot added the label CI failed on May 15, 2024
  6. DrahtBot commented at 10:56 am on May 15, 2024: contributor

    🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the documentation.

    Possibly this is due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    Leave a comment here, if you need help tracking down a confusing failure.

    Debug: https://github.com/bitcoin/bitcoin/runs/24992798939

  7. glozow force-pushed on May 15, 2024
  8. DrahtBot added the label Needs rebase on May 15, 2024
  9. glozow force-pushed on May 15, 2024
  10. glozow force-pushed on May 15, 2024
  11. DrahtBot removed the label Needs rebase on May 15, 2024
  12. glozow force-pushed on May 17, 2024
  13. glozow force-pushed on May 20, 2024
  14. glozow force-pushed on May 20, 2024
  15. glozow commented at 10:55 am on May 20, 2024: member
    Rebased for #29817 and added a “ensure we can always download a tx as long as we have 1 good outbound peer” fuzz test
  16. in src/node/txdownload_impl.h:32 in b1cc0c3006 outdated
    27+static constexpr auto NONPREF_PEER_TX_DELAY{2s};
    28+/** How long to delay requesting transactions from overloaded peers (see MAX_PEER_TX_REQUEST_IN_FLIGHT). */
    29+static constexpr auto OVERLOADED_PEER_TX_DELAY{2s};
    30+/** How long to wait before downloading a transaction from an additional peer */
    31+static constexpr auto GETDATA_TX_INTERVAL{60s};
    32+struct TxDownloadOptions {
    


    dergoegge commented at 11:55 am on May 20, 2024:
    Structs that are part of the public interface should probably go into txdownloadman.h. Otherwise one needs to include the impl header, e.g. like txdownloadman.h does right now, which defeats the purpose of pimpling to some extend.

    glozow commented at 12:49 pm on May 21, 2024:

    Hm, in the previous setup, we can change any of txdownload_impl.cpp without net_processing.cpp noticing, but yeah maybe less clean to depend on txdownload_impl.h.

    I’ve updated with a new setup where I’ve added a txdownloadman.cpp depending on txdownload_impl.h, and txdownloadman.h is included by txdownload_impl.h instead of the other way around. And so net_processing.cpp no longer depends on txdownload_impl.h, which makes sense to me. Had to update lint-circular-dependencies.py but I do like that we can change txdownload_impl.h without recompiling everything. lmk if this seems better?

  17. in src/node/txdownload_impl.cpp:51 in b1cc0c3006 outdated
    50+    m_recent_confirmed_transactions.reset();
    51+}
    52+
    53+bool TxDownloadImpl::AlreadyHaveTx(const GenTxid& gtxid, bool include_reconsiderable)
    54+{
    55+    const uint256& hash = gtxid.GetHash();
    


    dergoegge commented at 12:29 pm on May 20, 2024:

    Iiuc correctly, clearing the filters here has been removed as we don’t want TxDownloadManager to depend on ChainstateManager.

    An alternative to the new synchronous validation interface callback could be to preserve the previous behavior (clearing the filters in AlreadyHaveTx on a tip change) by introducing an interface for TxDownloadMan to fetch the latest tip hash. For example, TxDownloadOptions could be extended to hold a lambda that returns the latest tip hash, which in production simply fetches the hash from the ChainstateManager (and for tests it can be mocked). This might be easier to review since it preserves the previous mechanism? Although perhaps also more difficult w.r.t to lock inversion of cs_main and m_tx_download_mutex.

    Another alternative could be to directly pass the current block hash as a param into the relevant TxDownloadManager methods.


    glozow commented at 12:43 pm on May 21, 2024:

    I don’t think we’d ever need to make TxDownloadManager depend on ChainstateManager as we could just pass in the block hash, i.e.

    Another alternative could be to directly pass the current block hash as a param into the relevant TxDownloadManager methods.

    This would avoid the addition of UpdateBlockTipSync etc, as it’s basically what we do now. I find this way of keeping synchronized with the chain tip pretty ugly - we’d need to hold cs_main and pass the blockhash to the TxDownloadManager all the time, bloating the interface. Subscribing to the validation interface and updating on every chain tip update seems much more sensible.

  18. DrahtBot removed the label CI failed on May 20, 2024
  19. glozow force-pushed on May 21, 2024
  20. DrahtBot added the label Needs rebase on Jun 3, 2024
  21. glozow force-pushed on Jun 6, 2024
  22. glozow commented at 12:36 pm on June 6, 2024: member
    rebased
  23. DrahtBot removed the label Needs rebase on Jun 6, 2024
  24. DrahtBot added the label Needs rebase on Jun 10, 2024
  25. glozow force-pushed on Jun 11, 2024
  26. DrahtBot removed the label Needs rebase on Jun 11, 2024
  27. DrahtBot commented at 11:13 am on June 11, 2024: contributor

    🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the documentation.

    Possibly this is due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    Leave a comment here, if you need help tracking down a confusing failure.

    Debug: https://github.com/bitcoin/bitcoin/runs/26067441690

  28. DrahtBot added the label CI failed on Jun 11, 2024
  29. glozow force-pushed on Jun 11, 2024
  30. DrahtBot removed the label CI failed on Jun 11, 2024
  31. DrahtBot added the label Needs rebase on Jun 17, 2024
  32. glozow commented at 8:53 am on June 19, 2024: member
    Planning to rebase on master along with #30111 when it’s merged
  33. glozow force-pushed on Jul 16, 2024
  34. DrahtBot added the label CI failed on Jul 16, 2024
  35. DrahtBot commented at 4:06 pm on July 16, 2024: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/27514949081

    Make sure to run all tests locally, according to the documentation.

    The failure may happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  36. DrahtBot removed the label Needs rebase on Jul 16, 2024
  37. fanquake referenced this in commit 9607277032 on Jul 24, 2024
  38. glozow force-pushed on Jul 24, 2024
  39. glozow force-pushed on Jul 25, 2024
  40. glozow marked this as ready for review on Jul 25, 2024
  41. DrahtBot removed the label CI failed on Jul 25, 2024
  42. glozow commented at 12:14 pm on July 26, 2024: member
    Rebased, ready for review.
  43. DrahtBot added the label Needs rebase on Aug 1, 2024
  44. glozow force-pushed on Aug 1, 2024
  45. glozow commented at 1:29 pm on August 1, 2024: member
    Rebased for #30413
  46. DrahtBot removed the label Needs rebase on Aug 1, 2024
  47. glozow renamed this:
    refactor: TxDownloadManager
    refactor: TxDownloadManager + fuzzing
    on Aug 8, 2024
  48. Theschorpioen approved
  49. in src/node/txdownload_impl.cpp:16 in 41080ff0e9 outdated
    11+#include <validationinterface.h>
    12+
    13 namespace node {
    14+void TxDownloadImpl::ActiveTipChange()
    15+{
    16+    // If the chain tip has changed, previously rejected transactions might now be invalid, e.g. due
    


    theStack commented at 4:28 pm on August 13, 2024:
    0    // If the chain tip has changed, previously rejected transactions might now be valid, e.g. due
    

    also, this comment now exists both in PeerManagerImpl::ActiveTipChange and TxDownloadImpl::ActiveTipChange, probably one can be removed


    glozow commented at 9:13 am on August 14, 2024:
    Ah, that looks like a bad rebase - deleted the comment now, thanks
  50. in src/node/txdownload_impl.cpp:83 in 6c70bc621b outdated
    75@@ -75,4 +76,23 @@ bool TxDownloadImpl::AlreadyHaveTx(const GenTxid& gtxid, bool include_reconsider
    76 
    77     return RecentRejectsFilter().contains(hash) || m_opts.m_mempool.exists(gtxid);
    78 }
    79+
    80+void TxDownloadImpl::ConnectedPeer(NodeId nodeid, const TxDownloadConnectionInfo& info)
    81+{
    82+    // If already connected (shouldn't happen in practice), exit early.
    83+    if (m_peer_info.count(nodeid) > 0) return;
    


    theStack commented at 4:33 pm on August 13, 2024:

    slightly-more-modern-nit (here and in TxDownloadImpl::DisconnectedPeer below):

    0    if (m_peer_info.contains(nodeid)) return;
    

    glozow commented at 9:18 am on August 14, 2024:
    done :+1:
  51. in src/net_processing.cpp:3942 in 3f5dbd56da outdated
    4072-                if (!fAlreadyHave && !m_chainman.IsInitialBlockDownload()) {
    4073-                    AddTxAnnouncement(pfrom, gtxid, current_time);
    4074+                if (!m_chainman.IsInitialBlockDownload()) {
    4075+                    const bool fAlreadyHave{m_txdownloadman.AddTxAnnouncement(pfrom.GetId(), gtxid, current_time, /*p2p_inv=*/true)};
    4076+                    LogPrint(BCLog::NET, "got inv: %s  %s peer=%d\n", inv.ToString(), fAlreadyHave ? "have" : "new", pfrom.GetId());
    4077                 }
    


    theStack commented at 4:48 pm on August 13, 2024:
    nit: strictly speaking this is not a pure refactor, as there is a change in logging behaviour (INV messages received during IBD now don’t trigger the “got inv” log message any more). seems to make more sense though anyway, so feel free to ignore

    glozow commented at 9:55 am on August 14, 2024:

    Right, I don’t think it’s problematic to not log tx invs during IBD since they are ignored anyway.

    I’ve moved the logging change to its own commit, to keep the pure refactoring-ness of this commit :+1:

  52. in src/net_processing.cpp:4482 in 3f5dbd56da outdated
    4477@@ -4531,7 +4478,9 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
    4478                     AddKnownTx(*peer, parent_txid);
    4479                     // Exclude m_lazy_recent_rejects_reconsiderable: the missing parent may have been
    4480                     // previously rejected for being too low feerate. This orphan might CPFP it.
    4481-                    if (!m_txdownloadman.AlreadyHaveTx(gtxid, /*include_reconsiderable=*/false)) AddTxAnnouncement(pfrom, gtxid, current_time);
    4482+                    if (!m_txdownloadman.AlreadyHaveTx(gtxid, /*include_reconsiderable=*/false)) {
    4483+                        m_txdownloadman.AddTxAnnouncement(pfrom.GetId(), gtxid, current_time, /*p2p_inv=*/false);
    


    theStack commented at 4:55 pm on August 13, 2024:
    small note: with this approach, AlreadyHaveTx is now called twice: once in the if condition, and the second time in the AddTxAnnouncement method (not sure if that’s really a problem though)

    glozow commented at 9:55 am on August 14, 2024:
    Ah, good observation. I’ve now gated the AlreadyHaveTx check inside of AddTxAnnouncement on p2p_inv so this is no longer happening.

    instagibbs commented at 6:41 pm on August 19, 2024:

    It may have been an issue since the AddTxAnnouncement invocation of AlreadyHaveTx included reconsiderable filter.

    Since there are only two call sites of AddTxAnnouncement, and each had a filtering AlreadyHasTx call, would it make sense to just do something like this instead?

    0if (AlreadyHaveTx(gtxid, /*include_reconsiderable=*/p2p_inv)) return true;
    
  53. theStack commented at 5:43 pm on August 13, 2024: contributor

    Concept ACK

    Lightly reviewed about half of the refactoring commits, left some non-critical stuff and comments on the way below.

  54. glozow force-pushed on Aug 14, 2024
  55. glozow commented at 9:55 am on August 14, 2024: member
    Thanks @theStack!
  56. glozow commented at 10:32 am on August 14, 2024: member
    Would it help if I put the test/fuzz changes in a separate PR from the refactoring changes? It would make the first PR less exciting to review, but perhaps easier to review?
  57. maflcko commented at 10:50 am on August 14, 2024: member

    Would it help if I put the test/fuzz changes in a separate PR from the refactoring changes? It would make the first PR less exciting to review, but perhaps easier to review?

    Up to you, but there is a small benefit of going with the fuzz tests first (if possible), in that they first run on master (and pass), and then on the refactor changes (and pass as well). For unit tests this can be achieved by just putting them in an early commit and then running the tests for all commits. For fuzz tests, it is a bit more difficult, because fuzz inputs need to exist. (Also, some fuzz task sanitizers are only run in external CI, because they take a long time)

  58. dergoegge commented at 11:37 am on August 14, 2024: member

    Up to you, but there is a small benefit of going with the fuzz tests first (if possible)

    It looks like the tests here mostly operate on the split out modules after the refactoring, as opposed to a implementation agnostic test through the p2p interface (which would be a giant pita to write). So splitting the fuzz test out would be fine as they don’t directly assert that no behavior changes are made. It would of course be sad if we split them out and then only the refactoring lands (which has happened before).

  59. glozow commented at 12:12 pm on August 14, 2024: member

    to you, but there is a small benefit of going with the fuzz tests first (if possible)

    It looks like the tests here mostly operate on the split out modules after the refactoring, as opposed to a implementation agnostic test through the p2p interface (which would be a giant pita to write).

    Indeed, all of these tests require the refactors (on master we’d have to create a chain to fire validation events + construct actual transactions with each type of invalidity).

    So splitting the fuzz test out would be fine as they don’t directly assert that no behavior changes are made. It would of course be sad if we split them out and then only the refactoring lands (which has happened before).

    If people like the fuzz tests (concept acks would be nice), I’ll split them off to make this PR smaller, and keep the unit tests in here. I just don’t want to create 2 big PRs that nobody is interested in.

  60. instagibbs commented at 2:11 pm on August 14, 2024: member
    personally speaking I’m unsure what is gained by moving the fuzz tests to their own PR since they depend on the refactors. I don’t think we want to merge the other commits unless the fuzz harnesses are attached to it. I’m continuing to review either way.
  61. glozow commented at 2:45 pm on August 14, 2024: member

    I don’t think we want to merge the other commits unless the fuzz harnesses are attached to it. I’m continuing to review either way.

    Ok sounds good, I’ll leave as is. Just hoping to make review easier.

  62. hebasto added the label Needs CMake port on Aug 16, 2024
  63. in src/node/txdownload_impl.cpp:48 in 3824d1f81a outdated
    43@@ -44,4 +44,33 @@ void TxDownloadImpl::BlockDisconnected()
    44     // should be just after a new block containing it is found.
    45     RecentConfirmedTransactionsFilter().reset();
    46 }
    47+
    48+bool TxDownloadImpl::AlreadyHaveTx(const GenTxid& gtxid, bool include_reconsiderable)
    


    instagibbs commented at 4:53 pm on August 19, 2024:
    3824d1f81ad9e4ff44ec46d07f4e8f86f27ce643 decent time to rename the function since that’s been a previous discussion point?

    glozow commented at 9:56 am on August 21, 2024:
    What should the new name be?
  64. in src/net_processing.cpp:3843 in af3647050a outdated
    3839@@ -3841,6 +3840,14 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
    3840                        tx_relay->m_next_inv_send_time == 0s));
    3841         }
    3842 
    3843+        LOCK2(::cs_main, m_tx_download_mutex);
    


    instagibbs commented at 6:21 pm on August 19, 2024:
    af3647050a85e6f33f902b903207043f6f0c023c preference for this to be inside own scope

    glozow commented at 10:24 am on August 21, 2024:
    done
  65. in src/net_processing.cpp:3010 in 77560a690c outdated
    3006         ptx->GetWitnessHash().ToString(),
    3007         nodeid,
    3008         state.ToString());
    3009 
    3010-    if (state.GetResult() == TxValidationResult::TX_MISSING_INPUTS) {
    3011+    // Only process a new orphan if maybe_add_extra_compact_tx, as otherwise it must be either
    


    instagibbs commented at 7:44 pm on August 19, 2024:

    maybe_add_extra_compact_tx is starting to creep in usage vs its ostensible usage

    I’m also not quire sure I parse this sentence. I presume it’s trying to say this is only used in regular situations when freshly received over p2p.


    glozow commented at 10:37 am on August 21, 2024:

    Renamed it to first_time_failure in a preceding commit.

    Yeah that’s correct. Basically, if this is the first time a tx is being rejected from mempool, we consider putting it in vExtraTxnForCompact. If it’s not the first time, we don’t need to do it again, presumably because we don’t want duplicates (it’s a ring buffer).

    Same thing for adding something as an orphan. If this bool is false, it means the tx is already an orphan or it’s the low feerate parent in a package. Either way we don’t need to bother with the unique_parents and filter checking stuff.


    instagibbs commented at 6:28 pm on August 21, 2024:
    new name is better and makes flow easier to understand,thanks
  66. in src/net_processing.cpp:3082 in 77560a690c outdated
    3078+            // parents so avoid re-requesting it from other peers.
    3079+            // Here we add both the txid and the wtxid, as we know that
    3080+            // regardless of what witness is provided, we will not accept
    3081+            // this, so we don't need to allow for redownload of this txid
    3082+            // from any of our non-wtxidrelay peers.
    3083+            RecentRejectsFilter().insert(tx.GetHash().ToUint256());
    


    instagibbs commented at 7:49 pm on August 19, 2024:
    unrelated to your move-only change, but if txid==wtxid, looks like we’re doing two insertions anyways here

    glozow commented at 10:43 am on August 21, 2024:
    agree, though leaving out of this PR to limit its scope. Could add a nonsegwit parent + segwit child test to be added to p2p_orphan_handling.py. Maybe a good first issue.
  67. in src/net_processing.cpp:607 in d3fb8ca103 outdated
    603@@ -594,8 +604,8 @@ class PeerManagerImpl final : public PeerManager
    604      *                                            Set to false if the tx has already been rejected before,
    605      *                                            e.g. is an orphan, to avoid adding duplicate entries.
    606      * Updates m_txrequest, m_lazy_recent_rejects, m_lazy_recent_rejects_reconsiderable, m_orphanage, and vExtraTxnForCompact. */
    607-    void ProcessInvalidTx(NodeId nodeid, const CTransactionRef& tx, const TxValidationState& result,
    608-                          bool maybe_add_extra_compact_tx)
    609+    std::optional<PackageToValidate> ProcessInvalidTx(NodeId nodeid, const CTransactionRef& tx, const TxValidationState& result,
    


    instagibbs commented at 7:51 pm on August 19, 2024:

    d3fb8ca103b32f6071790bf7a9471fe4f80f5479

    new ProcessInvalidTx return value needs to be documented


    glozow commented at 11:04 am on August 21, 2024:
    added
  68. in src/net_processing.cpp:3121 in d3fb8ca103 outdated
    3116@@ -3104,6 +3117,14 @@ void PeerManagerImpl::ProcessInvalidTx(NodeId nodeid, const CTransactionRef& ptx
    3117             // because we should not download or submit this transaction by itself again, but may
    3118             // submit it as part of a package later.
    3119             RecentRejectsReconsiderableFilter().insert(ptx->GetWitnessHash().ToUint256());
    3120+
    3121+            if (maybe_add_extra_compact_tx) {
    


    instagibbs commented at 7:52 pm on August 19, 2024:
    further definitional drift of maybe_add_extra_compact_tx, let’s try to find something else to call this?

    glozow commented at 11:04 am on August 21, 2024:
    done
  69. in src/net_processing.cpp:2998 in 65d773feb7 outdated
    3138-    m_txrequest.ForgetTxHash(tx->GetWitnessHash());
    3139-
    3140-    m_orphanage.AddChildrenToWorkSet(*tx);
    3141-    // If it came from the orphanage, remove it. No-op if the tx is not in txorphanage.
    3142-    m_orphanage.EraseTx(tx->GetWitnessHash());
    3143+    m_txdownloadman.MempoolAcceptedTx(tx);
    


    instagibbs commented at 1:01 pm on August 20, 2024:

    65d773feb7384a6893ee7e6a7b24dfa60f2d8d39

    commit message seems grammatically incorrect:

    “[refactor] move valid and tx processing to TxDownload”


    glozow commented at 11:19 am on August 21, 2024:
    fixed
  70. in src/net_processing.cpp:4234 in ed96f3ef3d outdated
    4231-        // already; and an adversary can already relay us old transactions
    4232-        // (older than our recency filter) if trying to DoS us, without any need
    4233-        // for witness malleation.
    4234-        if (m_txdownloadman.AlreadyHaveTx(GenTxid::Wtxid(wtxid), /*include_reconsiderable=*/true)) {
    4235+        const auto& [should_validate, package_to_validate] = m_txdownloadman.ReceivedTx(pfrom.GetId(), ptx);
    4236+        if (!should_validate) {
    


    instagibbs commented at 1:22 pm on August 20, 2024:
    should Assume that package_to_validate is std::nullopt if should_validate is true

    glozow commented at 11:20 am on August 21, 2024:
    added

    instagibbs commented at 1:17 pm on August 21, 2024:
    think you added the other way, that if should_validate is false, it must have a package to validate, which is incorrect(and caught by our 1p1c tests)

    glozow commented at 2:44 pm on August 21, 2024:
    woops, fixed now
  71. in src/node/txdownload_impl.cpp:408 in ed96f3ef3d outdated
    403+            LogPrint(BCLog::TXPACKAGES, "found tx %s (wtxid=%s) in reconsiderable rejects, looking for child in orphanage\n",
    404+                     txid.ToString(), wtxid.ToString());
    405+            return std::make_pair(false, Find1P1CPackage(ptx, nodeid));
    406+        }
    407+
    408+        // If a tx is detected by m_lazy_recent_rejects it is ignored. Because we haven't
    


    instagibbs commented at 1:29 pm on August 20, 2024:
    just noting this moved comment is extremely verbose/old comment and can probably just have the last paragraph kept?

    glozow commented at 9:55 am on August 21, 2024:
    Seems fine to do, but won’t add it to this PR because I want to limit its scope. Agree just the last paragraph is sufficient.
  72. in src/node/txdownload_impl.cpp:429 in 81f67607e8 outdated
    424@@ -425,4 +425,15 @@ std::pair<bool, std::optional<PackageToValidate>> TxDownloadImpl::ReceivedTx(Nod
    425 
    426     return std::make_pair(true, std::nullopt);
    427 }
    428+
    429+bool TxDownloadImpl::HaveMoreWork(NodeId nodeid)
    


    instagibbs commented at 1:47 pm on August 20, 2024:
    If the wrapper is named HaveMoreWork, maybe have GetTxToReconsider be called GetMoreWork? Otherwise they seem incongruous.

    glozow commented at 9:46 am on August 21, 2024:

    I called it HaveMoreWork since its purpose is to tell ThreadMessageHandler() if there is more work to do (doesn’t matter what kind of work): https://github.com/bitcoin/bitcoin/blob/3c0b66c2ece7695ab59f466d7b4d81a4c18ffd76/src/net_processing.cpp#L5083 https://github.com/bitcoin/bitcoin/blob/3c0b66c2ece7695ab59f466d7b4d81a4c18ffd76/src/net.cpp#L2902-L2903

    GetTxToReconsider’s user is peerman, in ProcessOrphanTx.

    I think scheduled reconsideration work could become more generic in the future, i.e. include packages. But I think ProcessOrphanTx is probably more readable if the function name specifies we’re getting 1 transaction to reconsider.

  73. in src/node/txdownload_impl.cpp:439 in 388c2f5d7e outdated
    435@@ -436,4 +436,13 @@ CTransactionRef TxDownloadImpl::GetTxToReconsider(NodeId nodeid)
    436     return m_orphanage.GetTxToReconsider(nodeid);
    437 }
    438 
    439+void TxDownloadImpl::CheckIsEmpty(NodeId nodeid)
    


    instagibbs commented at 1:49 pm on August 20, 2024:
    CheckRequestsEmpty?

    glozow commented at 9:39 am on August 21, 2024:

    CheckIsEmpty() seems a bit more future-proof. We’ll check

    • no requests for this peer
    • no pending orphan resolutions for this peer
    • no orphans for this peer etc.
  74. instagibbs commented at 1:56 pm on August 20, 2024: member

    reviewed all refactorings through 388c2f5d7ee5f38cbefaff0c85cf298083127299

    Pretty straight forward the whole way through.

    going to spend time reviewing/testing the fuzz portions next

  75. in src/test/txdownload_tests.cpp:70 in c1609a92ce outdated
    65+    // Ensure every transaction has a different txid by having each one spend the previous one.
    66+    static Txid prevout_hash{};
    67+
    68+    std::vector<CTransactionRef> txns;
    69+    txns.reserve(num_txns);
    70+    // Simplest spk for every tx
    


    instagibbs commented at 2:17 pm on August 20, 2024:
    simplest is a blank CScript

    glozow commented at 12:30 pm on August 21, 2024:
    done
  76. in src/test/txdownload_tests.cpp:87 in c1609a92ce outdated
    82+    return txns;
    83+}
    84+static CTransactionRef GetNewTransaction()
    85+{
    86+    // Create 2 transactions and return the second one. Transactions are created as a chain to
    87+    // ensure uniqueness. Here, we want to avoid direct parent/child relationships between transactions.
    


    instagibbs commented at 2:18 pm on August 20, 2024:

    Here, we want to avoid direct parent/child relationships between transacti

    doesn’t a chain mean direct parent/child?


    glozow commented at 10:15 am on August 21, 2024:

    So the requirements are:

    • transactions need to be unique
    • they need to appear to not be related (i.e. no direct relationships)

    One way to do this is to make unique utxos for each tx. But we also know from experience debugging fuzzers that sometimes the random outpoint’s txid matches the hash of one of the transactions, which is annoying because it creates “direct relationships”.

    Ironically, since prevouts reference hashes, the easiest way to ensure transactions aren’t directly related is to have them all be indirectly related.

    So my quick and dirty method for creating n transactions is to generate 1 utxo and make 2*n transactions chaining off of it, then use every other tx. I don’t need to know n ahead of time - I just make 2 at the bottom whenever I need one. They’re guaranteed to be unique (because sha256) and to not look like they’re spending each other (because sha256).


    instagibbs commented at 6:41 pm on August 21, 2024:
    :facepalm: I was missing the static in front of prevout_hash. This is making a lot more sense now.

    instagibbs commented at 6:41 pm on August 21, 2024:
    might be worth a comment!
  77. in src/test/txdownload_tests.cpp:101 in c1609a92ce outdated
     96+
     97+    TxValidationState state;
     98+
     99+    for (const auto& [result, expected_behavior] : expected_behaviors) {
    100+        node::TxDownloadImpl txdownload_impl{node::TxDownloadOptions{pool, det_rand, DEFAULT_MAX_ORPHAN_TRANSACTIONS}};
    101+        const auto ptx = GetNewTransaction();
    


    instagibbs commented at 2:24 pm on August 20, 2024:

    ptx/txid/wtxid can just be made once outside the loop since it’s the same each time

    as well as nodeid, now


    glozow commented at 12:31 pm on August 21, 2024:
    done
  78. in src/test/txdownload_tests.cpp:86 in c1609a92ce outdated
    81+    }
    82+    return txns;
    83+}
    84+static CTransactionRef GetNewTransaction()
    85+{
    86+    // Create 2 transactions and return the second one. Transactions are created as a chain to
    


    instagibbs commented at 2:36 pm on August 20, 2024:

    what is this chaining doing? I only see the ultimate tx being used anyways.

    It might be useful to have the parent tx rejected optionally, to check that TX_MISSING_INPUTS, fills out txid and wtxid reject filters properly.


    glozow commented at 4:19 pm on August 21, 2024:
    added tests for orphans with rejected parents
  79. in src/test/fuzz/txdownloadman.cpp:168 in 2d6a41102e outdated
    149+    CTxMemPool pool{MemPoolOptionsForTest(g_setup->m_node), error};
    150+    const auto max_orphan_count = fuzzed_data_provider.ConsumeIntegralInRange<unsigned int>(0, 300);
    151+    FastRandomContext det_rand{true};
    152+    node::TxDownloadManager txdownloadman{node::TxDownloadOptions{pool, det_rand, max_orphan_count}};
    153+
    154+    std::chrono::microseconds time{244466666};
    


    instagibbs commented at 2:49 pm on August 20, 2024:
    1 2, 3 4s, 5 6s womp womp

    glozow commented at 2:45 pm on August 21, 2024:

    womp womp

    Can you be a bit more specific :joy:


    instagibbs commented at 3:02 pm on August 22, 2024:
    No. I just had to google this magic number and felt slightly rickrolled

    glozow commented at 3:04 pm on August 22, 2024:
    ohhh i get it now. i thought you found a crash and were giving a cryptic description of the input
  80. in src/test/fuzz/txdownloadman.cpp:177 in 2d6a41102e outdated
    158+        NodeId rand_peer = fuzzed_data_provider.ConsumeIntegralInRange<int64_t>(0, NUM_PEERS);
    159+
    160+        // Transaction can be one of the premade ones or a randomly generated one
    161+        auto rand_tx = fuzzed_data_provider.ConsumeBool() ?
    162+            MakeTransactionSpending({fuzzed_data_provider.PickValueInArray(COINS)},
    163+                                    /*num_outputs=*/fuzzed_data_provider.ConsumeIntegralInRange(1, 500),
    


    instagibbs commented at 2:54 pm on August 20, 2024:
    500 outputs is probably too much unless you think it’s gaining some meaningful coverage?

    glozow commented at 4:06 pm on August 21, 2024:
    It makes them potentially larger transactions?
  81. in src/test/fuzz/txdownloadman.cpp:215 in 2d6a41102e outdated
    210+                txdownloadman.ReceivedTx(rand_peer, rand_tx);
    211+                const auto& [should_validate, maybe_package] = txdownloadman.ReceivedTx(rand_peer, rand_tx);
    212+                Assert(should_validate || !maybe_package.has_value());
    213+                if (maybe_package.has_value()) {
    214+                    Assert(maybe_package->m_senders.size() == 2);
    215+                    Assert(maybe_package->m_senders.front() == rand_peer);
    


    instagibbs commented at 2:58 pm on August 20, 2024:
    m_senders.back() should be <= NUM_PEERS

    glozow commented at 4:07 pm on August 21, 2024:
    done
  82. in src/test/fuzz/txdownloadman.cpp:433 in 2d6a41102e outdated
    387+                    txdownload_impl.MempoolRejectedTx(ptx, state_missing_inputs, rand_peer, fuzzed_data_provider.ConsumeBool());
    388+                }
    389+            }
    390+        );
    391+
    392+        // Jump ahead in time
    


    instagibbs commented at 2:59 pm on August 20, 2024:
    not just ahead due to negative skips

    glozow commented at 4:08 pm on August 21, 2024:
    I thought they were all positive?

    instagibbs commented at 11:30 am on August 22, 2024:
    hmm, must have been referencing something else, nevermind
  83. in src/test/fuzz/txdownloadman.cpp:336 in 2d6a41102e outdated
    300+                txdownload_impl.DisconnectedPeer(rand_peer);
    301+                txdownload_impl.CheckIsEmpty(rand_peer);
    302+            },
    303+            [&] {
    304+                txdownload_impl.ActiveTipChange();
    305+                // After a block update, nothing should be in the rejection caches
    


    instagibbs commented at 3:04 pm on August 20, 2024:
    iterate over all TRANSACTIONS and assert this?

    glozow commented at 4:08 pm on August 21, 2024:
    done
  84. in src/test/fuzz/txdownloadman.cpp:292 in 2d6a41102e outdated
    257+    // We should never have more than the maximum in-flight requests out for a peer.
    258+    for (NodeId peer = 0; peer < NUM_PEERS; ++peer) {
    259+        if (!HasRelayPermissions(peer)) {
    260+            Assert(txdownload_impl.m_txrequest.CountInFlight(peer) <= node::MAX_PEER_TX_REQUEST_IN_FLIGHT);
    261+        }
    262+    }
    


    instagibbs commented at 3:11 pm on August 20, 2024:
    0    }
    1    txdownload_impl.m_txrequest.SanityCheck();
    

    glozow commented at 4:09 pm on August 21, 2024:
    added
  85. in src/test/fuzz/txdownloadman.cpp:361 in 2d6a41102e outdated
    323+            [&] {
    324+                txdownload_impl.MempoolAcceptedTx(rand_tx);
    325+            },
    326+            [&] {
    327+                TxValidationState state;
    328+                state.Invalid(fuzzed_data_provider.PickValueInArray(TESTED_TX_RESULTS), "");
    


    instagibbs commented at 3:13 pm on August 20, 2024:
    MempoolRejectedTx should probably Assume that state is invalid at the top of its definition

    glozow commented at 4:18 pm on August 21, 2024:
    should it? seems more complete if it handles all types, and we test them

    instagibbs commented at 11:29 am on August 22, 2024:
    The fuzz target is always setting state.Invalid, so I don’t see where this coverage would come from?

    glozow commented at 10:18 pm on October 1, 2024:
    Added the Assume. I agree it makes more sense.
  86. in src/test/fuzz/txdownloadman.cpp:399 in 2d6a41102e outdated
    353+                    Assert(maybe_package->m_senders.size() == 2);
    354+                    Assert(maybe_package->m_senders.front() == rand_peer);
    355+
    356+                    const auto& package = maybe_package->m_txns;
    357+                    Assert(package.size() == 2);
    358+                    // Parent is in m_lazy_recent_rejects_reconsiderable and child is in m_orphanage
    


    instagibbs commented at 3:23 pm on August 20, 2024:

    Assert(!txdownload_impl.RecentRejectsReconsiderableFilter().contains(maybe_package->m_txns.back()->GetWitnessHash().ToUint256()));

    I considered this assertion, is it valid? Can we have something in our orphanage and reconsiderable filter?


    glozow commented at 4:11 pm on August 21, 2024:
    I think something would be wrong then. It wouldn’t be missing inputs if we knew what the fee was.
  87. in src/test/fuzz/txdownloadman.cpp:415 in 2d6a41102e outdated
    369+                txdownload_impl.ReceivedNotFound(rand_peer, {rand_tx->GetWitnessHash()});
    370+            },
    371+            [&] {
    372+                const bool expect_work{txdownload_impl.HaveMoreWork(rand_peer)};
    373+                const auto ptx = txdownload_impl.GetTxToReconsider(rand_peer);
    374+                // expect_work=true doesn't necessarily mean the next item from the workset isn't a
    


    instagibbs commented at 3:28 pm on August 20, 2024:
    Can you remind me of a case where this happens? Running with an erroneous assertion added and it’s not seeming to hit.

    glozow commented at 4:13 pm on August 21, 2024:
    1. tx is added to workset
    2. tx is removed from orphanage
    3. peer workset is nonempty, but its wtxids correspond to stuff that isn’t there anymore
  88. in src/test/fuzz/tx_download.cpp:170 in 04856ed32c outdated
    165+    TestPeer peer;
    166+    peer.m_nodeid = nodeid;
    167+    peer.m_good = force_good || fuzzed_data_provider.ConsumeBool();
    168+    peer.m_connection_preferred = preferred;
    169+    peer.m_relay_permissions = false;
    170+    peer.m_wtxid_relay = force_good || fuzzed_data_provider.ConsumeBool();
    


    instagibbs commented at 3:37 pm on August 20, 2024:
    maybe leave a comment why good peers do wtxid relay

    glozow commented at 4:18 pm on August 21, 2024:
    added
  89. in src/test/fuzz/tx_download.cpp:114 in 04856ed32c outdated
    109+    bool m_good;
    110+
    111+    // for TxDownloadConnectionInfo
    112+    // Note that this is used to indicate when somebody is an outbound peer.
    113+    bool m_connection_preferred{false};
    114+    bool m_relay_permissions{false};
    


    instagibbs commented at 4:23 pm on August 20, 2024:
    seems to always be false?

    glozow commented at 4:18 pm on August 21, 2024:
    changed to come from fuzzer
  90. instagibbs commented at 8:48 pm on August 20, 2024: member

    ok I reviewed the unit and fuzz tests.

    For the tx_download fuzz target there’s a couple issues:

    1. the time could adversarially be moved forward only 1ms at a time, 100k iterations results in 100 seconds. I hacked the target to only allow the forced good peer to respond to the getdata. I got a case where the while loop exits when only 100 seconds have passed in simulated time. Stalling peer A refuses to send TX, then after 60 seconds, at the same time, an honest peer and stalling peer connect and offer the same INV. First INV times out, then second stalling peer gets chosen as next CANIDATE_BEST. Runs out of iterations 40 seconds later.

    2. More annoyingly, the case I found had instability. I think it’s because TxRequests’ PriorityComputer is not being set to deterministic random even when TxDownloadMan is. So sometimes the case was passing due to the ordering of the candidates based on this randomness.

    In general I think this last fuzz target should try to target more on the end of “only one good peer”, at least switch to that with a bool, since I think when allowing all peers to perhaps be somewhat good, it hides potential issues.

  91. glozow force-pushed on Aug 21, 2024
  92. glozow commented at 11:55 am on August 21, 2024: member
    Pushed to address the first batch of comments, going to work on test parts next
  93. dergoegge commented at 12:53 pm on August 21, 2024: member

    The naming of the files and tests is a bit inconsistent, I would suggest the following:

    • txdownloadman.h, txdownloadman.cpp, txdownloadman_impl.h and txdownloadman_impl.cpp with the classes: TxDownloadManager and TxDownloadManagerImpl
    • fuzz/txdownloadman.cpp: txdownloadman + txdownloadman_impl
    • fuzz/txdownloadman_one_honest_peer.cpp: txdownloadman_one_honest_peer

    I don’t care about the specific names just the consistency, e.g. don’t mix tx_download and txdownload or txdownloadman and txdownload. The test names should roughly convey what is being tested as well.

    I’m fuzzing all the harnesses atm, should have coverage reports up by tomorrow.

  94. DrahtBot added the label CI failed on Aug 21, 2024
  95. glozow force-pushed on Aug 21, 2024
  96. glozow force-pushed on Aug 21, 2024
  97. glozow force-pushed on Aug 21, 2024
  98. DrahtBot removed the label CI failed on Aug 21, 2024
  99. in src/test/txdownload_tests.cpp:238 in 8f5163e8a1 outdated
    227+
    228+    // Reconsiderable parent of a tx already in orphanage: the only time PackageToValidate is returned.
    229+    {
    230+        node::TxDownloadImpl txdownload_impl{node::TxDownloadOptions{pool, det_rand, DEFAULT_MAX_ORPHAN_TRANSACTIONS}};
    231+
    232+        // Child missing inputs, should be added to orphanage.
    


    instagibbs commented at 7:00 pm on August 21, 2024:
    nit: just assert it’s in orphanage instead

    glozow commented at 9:33 am on August 23, 2024:
    added
  100. instagibbs commented at 7:06 pm on August 21, 2024: member

    reviewed through bbec8e261467637ce26ecf551b528d9061cf4ffe

    via git range-diff master 388c2f5d7ee5f38cbefaff0c85cf298083127299 bbec8e261467637ce26ecf551b528d9061cf4ffe

    LGTM aside from waiting on fuzzer coverage results + fixes

  101. instagibbs commented at 1:53 pm on August 22, 2024: member

    Doesn’t look like ReceivedTx ever returns a package to validate in any of the fuzz tests (500 CPU hours each):

    At a minimum TESTED_TX_RESULTS is missing TxValidationResult::TX_RECONSIDERABLE, so nothing is ever failing for the right reason to try a package.

    I restricted the set of possible transactions down to just a parent/child combo in TRANSACTIONS and was able to trigger it quickly, so it’s likely reachable with that one change.

  102. in src/test/fuzz/txdownloadman.cpp:212 in 0de01c034e outdated
    207+                txdownloadman.GetRequestsToSend(rand_peer, time);
    208+            },
    209+            [&] {
    210+                txdownloadman.ReceivedTx(rand_peer, rand_tx);
    211+                const auto& [should_validate, maybe_package] = txdownloadman.ReceivedTx(rand_peer, rand_tx);
    212+                Assert(should_validate || !maybe_package.has_value());
    


    instagibbs commented at 3:02 pm on August 22, 2024:
    this fails if we have a package to validate…

    glozow commented at 9:32 am on August 23, 2024:
    I wrote the assertion incorrectly. The only combination that’s not ok is both.
  103. instagibbs commented at 3:06 pm on August 22, 2024: member

    3yTMzMzMzMzMzMzMzMw2lcV+LVVYVVX//y1maZY/xcWVxX5VWFVVVVUlugr///8tZmlVCg== for txdownloadman target

    if you add TX_RECONSIDERABLE to the end of TESTED_TX_RESULTS, this should crash because it found a package to try

  104. glozow force-pushed on Aug 23, 2024
  105. glozow commented at 9:32 am on August 23, 2024: member

    TESTED_TX_RESULTS is missing TxValidationResult::TX_RECONSIDERABLE

    That’s an oversight, I’ve added that and UNKNOWN now which should mean we see packages.

  106. in src/node/txdownloadman.h:101 in 6a84ab77f3 outdated
     96+    std::optional<PackageToValidate> m_package_to_validate;
     97+};
     98+
     99+
    100+class TxDownloadManager {
    101+    const std::unique_ptr<TxDownloadImpl> m_impl;
    


    dergoegge commented at 12:05 pm on August 23, 2024:
    0    const std::unique_ptr<TxDownloadManagerImpl> m_impl;
    

    👉👈


    glozow commented at 11:45 am on August 27, 2024:
    done

    ismaelsadeeq commented at 12:12 pm on August 28, 2024:
    The update was effected in the second commit

    glozow commented at 1:33 pm on August 28, 2024:
    Woops, ran the script from HEAD~24 instead of HEAD~25. Fixed now.
  107. in src/test/fuzz/txdownloadman_one_honest_peer.cpp:226 in 6a84ab77f3 outdated
    221+// malicious, we'll always be able to download a transaction.
    222+// Connect to 8 outbounds and 0-120 inbound peers. 1 outbound is guaranteed to be good (see m_good
    223+// above for definition), while the rest may or may not be.
    224+// A non-good node has a superset of the actions available to a good node: it may stall a getdata,
    225+// send a malleated or witness stripped tx/inv, send a notfound, etc.
    226+FUZZ_TARGET(tx_download_one_honest_peer, .init = initialize)
    


    dergoegge commented at 12:05 pm on August 23, 2024:
    0FUZZ_TARGET(txdownloadman_one_honest_peer, .init = initialize)
    

    👉👈


    glozow commented at 3:09 pm on August 23, 2024:
    done
  108. in src/test/fuzz/txdownloadman.cpp:280 in 6a84ab77f3 outdated
    275+        }
    276+    }
    277+    txdownload_impl.m_txrequest.SanityCheck();
    278+}
    279+
    280+FUZZ_TARGET(txdownload_impl, .init = initialize)
    


    dergoegge commented at 12:06 pm on August 23, 2024:
    0FUZZ_TARGET(txdownloadman_impl, .init = initialize)
    

    (if you take the other naming suggestion)

    👉👈


    glozow commented at 3:10 pm on August 23, 2024:
    done
  109. in src/test/fuzz/txdownloadman_one_honest_peer.cpp:284 in 6a84ab77f3 outdated
    279+    // within the first round and will never be disconnected.
    280+    int counter = 0;
    281+
    282+    LIMITED_WHILE(!got_tx, 100000)
    283+    {
    284+        std::shuffle(indexes.begin(), indexes.end(), det_rand);
    


    dergoegge commented at 12:20 pm on August 23, 2024:

    Any reason not to let the fuzzer decide the order?

    If the order is interesting (i.e. a specific order might trigger a bug) then I think it’d make more sense to let the fuzzer choose the order.


    glozow commented at 3:10 pm on August 23, 2024:
    done
  110. in src/node/txdownloadman.cpp:15 in 6a84ab77f3 outdated
    10+TxDownloadManager::TxDownloadManager(const TxDownloadOptions& options) :
    11+    m_impl{std::make_unique<TxDownloadImpl>(options)}
    12+{}
    13+TxDownloadManager::~TxDownloadManager() = default;
    14+
    15+void TxDownloadManager::ActiveTipChange()
    


    dergoegge commented at 12:25 pm on August 23, 2024:
    To avoid the circular dependency, you could get rid of this file and move its contents to txdownloadman_impl.cpp.

    glozow commented at 11:45 am on August 27, 2024:
    done
  111. dergoegge commented at 1:17 pm on August 23, 2024: member
    0$ FUZZ=txdownload_impl src/test/fuzz/fuzz solution-60b4aafd2c355198b269114d31545a12b9059fd6.txt
    1test/fuzz/txdownloadman.cpp:384 operator(): Assertion `!txdownload_impl.RecentRejectsFilter().contains(package.back()->GetWitnessHash().ToUint256())' failed.
    

    solution-60b4aafd2c355198b269114d31545a12b9059fd6.txt

  112. instagibbs commented at 1:42 pm on August 23, 2024: member

    solution-60b4aafd2c355198b269114d31545a12b9059fd6.txt

    Find1P1CPackage doesn’t actually check the normal reject filter at all with respect to the child, only reconsiderable, so that assertion gets hit. I suspect that’s something we don’t want it to do, even if it’s not possible to hit inside net_processing.

    LGTM modulo existing concerns about the tx_download_one_honest_peer harness:

    1. no det rand for tx_request module in downloadman, leading to instability
    2. timesteps can be so small that the tx is never downloaded in 100 seconds
    3. scenario should flip a bool to only allow the good peer to hand over real tx

    reviewed via git range-diff master bbec8e261467637ce26ecf551b528d9061cf4ffe 6a84ab77f36445fd37d46a08c71bd4373201d300

  113. glozow force-pushed on Aug 23, 2024
  114. glozow commented at 3:09 pm on August 23, 2024: member

    Find1P1CPackage doesn’t actually check the normal reject filter at all with respect to the child, only reconsiderable, so that assertion gets hit.

    I think it might be more that MempoolRejectedTx doesn’t remember if a tx failed for a different reason before considering an orphan?

  115. in src/net_processing.cpp:3083 in 1468112dda outdated
    3082             }
    3083 
    3084-            if (m_orphanage.AddTx(ptx, nodeid)) {
    3085-                AddToCompactExtraTransactions(ptx);
    3086-            }
    3087+            add_extra_compact_tx &= m_orphanage.AddTx(ptx, nodeid);
    


    theStack commented at 4:04 pm on August 23, 2024:
    just to be sure, was it intentional to use the bit-wise AND operator here, rather than the logical one (&&=), to avoid potential short-circuiting (see e.g. https://stackoverflow.com/questions/23107162/do-the-and-operators-for-bool-short-circuit)? IIUC, add_extra_compact_tx is always true at this point anyway (otherwise the if condition at the very top couldn’t be true), so could also just do a regular assignment, I guess. But not sure what’s better, one could argue that the current way is more future-proof.

    glozow commented at 11:46 am on August 27, 2024:
    Yes, we don’t want a short circuit, though in practice add_extra_compact_tx should always be true at this point in the code. I’ve added a comment describing what the expected changes can be.
  116. theStack commented at 4:24 pm on August 23, 2024: contributor

    Reviewed up to e164614e32010bc8061cc99ed486188235efa780 (i.e. everything modulo fuzz and unit tests in the last 3 commits), lgtm so far.

    Slightly off-topic, but: on some commits like 4e58d84da95d97d64f563b5c3e116769cd9ff89b --color-moved=dimmed-zebra didn’t detect any lines as move-only for me (with 2-3 exceptions that seem completely arbitrary), any suggestions for advanced git options and/or recommendations for other fancy diff tools that can cope with that? (Or well, maybe I should just try with a newer git version.)

  117. glozow force-pushed on Aug 27, 2024
  118. glozow commented at 11:49 am on August 27, 2024: member

    Thanks @dergoegge @theStack @instagibbs! Addressed all comments.

    Slightly off-topic, but: on some commits like https://github.com/bitcoin/bitcoin/commit/4e58d84da95d97d64f563b5c3e116769cd9ff89b –color-moved=dimmed-zebra didn’t detect any lines as move-only for me (with 2-3 exceptions that seem completely arbitrary), any suggestions for advanced git options and/or recommendations for other fancy diff tools that can cope with that?

    I see that happen too :( very annoying. Sometimes if I see there are indentation changes, adding a -w or manually (un)indenting helps the diff display better. Unfortunately I don’t have any other tips.

  119. instagibbs commented at 1:42 pm on August 27, 2024: member

    ACK 6f6fc1464ad2f2981e7c3ff5d6492f60f89a6504

    Attends to my concerns with the fuzz tests. Now in txdownloadman_one_honest_peer no “non-good” peer will ever deliver TX_REAL, ensuring that the test is actually testing what we want. Determinstic randomness was also given to the member txrequest modules.

    Lastly, minimum time skips were increased to 100ms, meaning 100ms*100000=~166 minutes of simulated time. Since there are up to 8+120=128 peers, this means each peer can stall out the node for a full minute and we still have 40 minutes left for the honest peer to succeed in giving the tx.

    If you touch this again, I might suggest adding some static asserts to the above effect in case people want to modify these values in the future.

  120. DrahtBot requested review from theStack on Aug 27, 2024
  121. instagibbs commented at 2:51 pm on August 27, 2024: member

    Just posting in case someone else learns something. Not asking for any changes:

    Lastly, minimum time skips were increased to 100ms, meaning 100ms*100000=~166 minutes of simulated time. Since there are up to 8+120=128 peers, this means each peer can stall out the node for a full minute and we still have 40 minutes left for the honest peer to succeed in giving the tx.

    After doing more study, I think this simulation “only” needs a little more than 7 minutes minimum time, since the internal PriorityComputer will always select outbounds(prefered) out of the set of CANDIDATE_READYs. Incoming connections will be passed over for the good(and outgoing) peer once the good peer sends INV_REAL when choosing new CANIDATE_BEST.

  122. in src/node/txdownloadman.h:16 in 58fb38c182 outdated
    11+class TxOrphanage;
    12+class TxRequestTracker;
    13+class CRollingBloomFilter;
    14+
    15+namespace node {
    16+class TxDownloadImpl;
    


    ismaelsadeeq commented at 11:36 am on August 28, 2024:

    At first I was wondering if their is any benefit of not declaring the implementation class private to TxDownloadManager as recommended in https://en.cppreference.com/w/cpp/language/pimpl.

    But then after the first pass of this PR, I think the reason is to make TxDownloadManagerImpl visible to the entire namespace in other to allow for testing TxDownloadManagerImpl specific behaviors in fuzzing.

  123. in src/node/txdownloadman_impl.cpp:4 in 58fb38c182 outdated
    0@@ -0,0 +1,34 @@
    1+// Copyright (c) 2024
    2+// Distributed under the MIT software license, see the accompanying
    3+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
    4+
    


    ismaelsadeeq commented at 11:48 am on August 28, 2024:

    In 58fb38c182906645161af04e84becc824dee2ba5 " [refactor] add TxDownloadManager wrapping TxOrphanage, TxRequestTracker, and bloom filters"

    I think the file name should be txdownloadman.cpp ? TxDownloadManager is the public interface.


    glozow commented at 1:18 pm on August 28, 2024:
    It contains the implementation for TxDownloadManagerImpl as well.
  124. in src/net_processing.cpp:1583 in 58fb38c182 outdated
    1581@@ -1672,6 +1582,8 @@ void PeerManagerImpl::AddTxAnnouncement(const CNode& node, const GenTxid& gtxid,
    1582     AssertLockHeld(::cs_main); // for State
    1583     AssertLockHeld(m_tx_download_mutex); // For m_txrequest
    


    ismaelsadeeq commented at 12:03 pm on August 28, 2024:
    0    AssertLockHeld(m_tx_download_mutex); // For m_txdownloadman
    

    glozow commented at 1:32 pm on August 28, 2024:
    Assuming this is on one of the earlier commits - the whole line is deleted in 0e6e135e303
  125. ismaelsadeeq commented at 12:57 pm on August 28, 2024: member
    Approach ACK
  126. glozow force-pushed on Aug 28, 2024
  127. glozow force-pushed on Aug 28, 2024
  128. glozow commented at 1:53 pm on August 28, 2024: member
    Rebased for cmake
  129. in src/test/txdownload_tests.cpp:124 in caedaec23a outdated
    94+
    95+    // A new TxDownloadManagerImpl is created for each tx so we can just reuse the same one.
    96+    TxValidationState state;
    97+    NodeId nodeid{0};
    98+    std::chrono::microseconds now{GetTime()};
    99+    node::TxDownloadConnectionInfo connection_info{/*m_preferred=*/false, /*m_relay_permissions=*/false, /*m_wtxid_relay=*/true};
    


    instagibbs commented at 2:33 pm on August 28, 2024:
    IIUC this would at least give more coverage to AddTxAnnouncement getting deeper during new orphan processing

    glozow commented at 2:40 pm on August 28, 2024:
    Right. I’m also building my orphan resolution branch that effectively skips adding orphans for peers that aren’t “registered” (we need to know their connection info to decide some params). These tests started failing there, so I had to add ConnectPeer. Figured it makes sense to do this now.
  130. instagibbs approved
  131. instagibbs commented at 2:34 pm on August 28, 2024: member

    reACK 2b387a156a1021ad782dc4de2fd34d5b5b81698a

    via git range-diff master 6f6fc1464ad2f2981e7c3ff5d6492f60f89a6504 2b387a156a1021ad782dc4de2fd34d5b5b81698a

    only non-cmake changes I see were reordering the class renaming, and the added ConnectedPeer in unit tests.

  132. DrahtBot requested review from ismaelsadeeq on Aug 28, 2024
  133. jonatack commented at 6:40 pm on August 28, 2024: member
    Interesting proposal to review; note to self to look at why move peer dis(connection) logic.
  134. hebasto removed the label Needs CMake port on Aug 29, 2024
  135. glozow force-pushed on Sep 2, 2024
  136. glozow commented at 6:36 pm on September 2, 2024: member
    Rebased for autotools removal, and made the txdownloadman_one_honest_peer fuzzer more interesting. Dishonest peers now have a lot of different transactions/invs they can send that can fail for more reasons.
  137. in src/test/fuzz/txdownloadman_one_honest_peer.cpp:44 in 1d4e33e7f8 outdated
    39+    NOTFOUND,
    40+};
    41+
    42+// What a peer may send in response to a getdata.
    43+enum class PossibleResponse {
    44+    /** Send the target transaction, in response to getdata */
    


    instagibbs commented at 8:42 pm on September 3, 2024:
    this comment is outdated, as TX_REAL now means really whatever the node getdata’d

    glozow commented at 2:25 pm on September 4, 2024:
    Yes, will fix if I retouch
  138. instagibbs approved
  139. instagibbs commented at 8:43 pm on September 3, 2024: member

    reACK 1d4e33e7f88162cf6fcd6aee0b63973015853591

    didn’t run the new fuzz test iteration though it seems reasonable to me

    via git range-diff master 2b387a156a1021ad782dc4de2fd34d5b5b81698a 1d4e33e7f88162cf6fcd6aee0b63973015853591

  140. in src/net_processing.cpp:4883 in c40cb85659 outdated
    5146+                    tx_invs.emplace_back(inv.hash);
    5147                 }
    5148             }
    5149         }
    5150+        LOCK(m_tx_download_mutex);
    5151+        m_txdownloadman.ReceivedNotFound(pfrom.GetId(), tx_invs);
    


    theStack commented at 1:52 am on September 4, 2024:
    in commit c40cb8565952f62732fa64875c99ef9082b9a6f4: now that the if-body above only reads and writes from local variables (vInv and tx_invs), acquiring the m_tx_download_mutex lock before the for-loop (line 5140 at this commit) is not needed anymore

    glozow commented at 12:48 pm on September 5, 2024:
    good point :+1:
  141. theStack approved
  142. theStack commented at 10:46 am on September 5, 2024: contributor

    Light review ACK 1d4e33e7f88162cf6fcd6aee0b63973015853591

    Reviewed the refactoring changes and the unit tests thouroughly, but didn’t ran or look in-depth at the fuzz tests.

  143. glozow commented at 12:57 pm on September 5, 2024: member

    Thanks for the reviews 🙏

    I’ve created a followups PR #30820

    After that will be the second part of #28031 (“try multiple peers for orphan resolution”). I’ll open a separate PR, since it has 140 comments. After that, I plan to open a PR that moves m_tx_download_mutex inside TxDownloadManagerImpl (i.e. make it internally thread-safe), which I mention in the PR description here. I had considered making it the next step, but I think waiting a little more is better because (1) It gives the fuzzers more time to run + test that txdownloadman can handle interleaved calls and stay internally inconsistent. (2) There is currently a function called from both peerman and within txdownloadman, but that will go away, making the lock changes cleaner.

  144. glozow requested review from dergoegge on Sep 6, 2024
  145. in src/node/txdownloadman_impl.h:118 in ecbbadf0f7 outdated
    113+    std::unique_ptr<CRollingBloomFilter> m_lazy_recent_confirmed_transactions{nullptr};
    114+
    115+    CRollingBloomFilter& RecentConfirmedTransactionsFilter()
    116+    {
    117+        if (!m_lazy_recent_confirmed_transactions) {
    118+            m_lazy_recent_confirmed_transactions = std::make_unique<CRollingBloomFilter>(120'000, 0.000'001);
    


    achow101 commented at 8:32 pm on September 16, 2024:

    In ecbbadf0f79bb32730d9e1400d84b84893e35e56 “[refactor] add TxDownloadManager wrapping TxOrphanage, TxRequestTracker, and bloom filters”

    Why have this bloom filter’s parameters changed? It was previously 48'000 and 0.000'001. If this was intentional, it should probably be a separate commit and a rationale provided.


    glozow commented at 0:32 am on September 17, 2024:
    good catch!! that was unintentional, reverted now
  146. in src/node/txdownloadman_impl.cpp:213 in c40cb85659 outdated
    206@@ -203,4 +207,13 @@ std::vector<GenTxid> TxDownloadManagerImpl::GetRequestsToSend(NodeId nodeid, std
    207     }
    208     return requests;
    209 }
    210+
    211+void TxDownloadManagerImpl::ReceivedNotFound(NodeId nodeid, const std::vector<uint256>& txhashes)
    212+{
    213+    for (const auto& txhash: txhashes) {
    


    achow101 commented at 8:59 pm on September 16, 2024:

    In c40cb8565952f62732fa64875c99ef9082b9a6f4 “[refactor] move notfound processing to txdownload”

    nit:

    0    for (const auto& txhash : txhashes) {
    

    glozow commented at 0:32 am on September 17, 2024:
    done
  147. in src/node/txdownloadman_impl.cpp:455 in f3cc632279 outdated
    450+    const uint256& txid = ptx->GetHash();
    451+    const uint256& wtxid = ptx->GetWitnessHash();
    452+
    453+    // Mark that we have received a response
    454+    m_txrequest.ReceivedResponse(nodeid, ptx->GetHash());
    455+    if (ptx->HasWitness()) m_txrequest.ReceivedResponse(nodeid, ptx->GetWitnessHash());
    


    achow101 commented at 9:19 pm on September 16, 2024:

    In f3cc6322798ca61a31f90b2ad7a0c32471db0410 “[refactor] move new tx logic to txdownload”

    nit:

    0    m_txrequest.ReceivedResponse(nodeid, txid);
    1    if (ptx->HasWitness()) m_txrequest.ReceivedResponse(nodeid, wtxid);
    

    glozow commented at 0:33 am on September 17, 2024:
    done
  148. [refactor] add TxDownloadManager wrapping TxOrphanage, TxRequestTracker, and bloom filters
    This module is going to be responsible for managing everything related
    to transaction download, including txrequest, orphan transactions and
    package relay. It will be responsible for managing usage of the
    TxOrphanage and instructing PeerManager:
    - what tx or package-related messages to send to which peer
    - whether a tx or package-related message is allowed or useful
    - what transactions are available to try accepting to mempool
    
    Future commits will consolidate the interface and re-delegate
    interactions from PeerManager to TxDownloadManager.
    85edaf2556
  149. glozow force-pushed on Sep 17, 2024
  150. glozow force-pushed on Sep 17, 2024
  151. marcofleon commented at 10:03 am on September 17, 2024: contributor

    Ran txdownloadman_impl and it crashed on this assertion:

    0operator(): Assertion `!txdownload_impl.RecentRejectsFilter().contains(package.front()->GetWitnessHash().ToUint256())' failed.
    

    Base64 of the input: 4d3Myytlcf9a3czLK2UxoFrjzMtlMQBSWt3Myyv//w==

    Input file if it’s any easier: txdownload_impl_crash.txt

  152. instagibbs commented at 1:46 pm on September 17, 2024: member

    @marcofleon Example checks out, it’s pretty simple:

    1. txA is called with txdownload_impl.MempoolRejectedTx and TX_RECONSIDERABLE
    2. txA is called with txdownload_impl.MempoolRejectedTx and TX_NOT_STANDARD or any failure that results in entry to reject filter
    3. ReceivedTx is called with txA, child is found in orphanage, package returned even though txA is in both filters.

    I think it’d make sense for ReceivedTx to abort trying to find a package if the parent is in reject filter, even if also in recent reject filter.

  153. in src/node/txdownloadman_impl.cpp:467 in 5724926a9b outdated
    447+}
    448+
    449+std::pair<bool, std::optional<PackageToValidate>> TxDownloadManagerImpl::ReceivedTx(NodeId nodeid, const CTransactionRef& ptx)
    450+{
    451+    const uint256& txid = ptx->GetHash();
    452+    const uint256& wtxid = ptx->GetWitnessHash();
    


    marcofleon commented at 5:52 pm on September 17, 2024:
    Why is it that we don’t initialize these with theTxid and Wtxid types? Have been thinking about making RelayTransaction type safe, so just wondering.

    glozow commented at 11:18 pm on September 17, 2024:
    txrequest and the bloom filters take uint256s, so this seemed easiest. I don’t feel strongly. RelayTransaction isn’t called here…?

    marcofleon commented at 9:16 am on September 18, 2024:
    Makes sense. Don’t feel strongly either, was asking for my own understanding. I meant the RelayTransaction comment as a separate topic/PR, referring to transaction types (uint256 vs Txid/Wxid) in general, not specific to this line. I guess, overall, trying to gauge how strongly we’re trying to adhere to the newer types.
  154. glozow force-pushed on Sep 17, 2024
  155. glozow commented at 11:45 pm on September 17, 2024: member

    Ran txdownloadman_impl and it crashed on this assertion:

    I think it’d make sense for ReceivedTx to abort trying to find a package if the parent is in reject filter, even if also in recent reject filter.

    Thanks @marcofleon @instagibbs! Was able to reproduce the crash.

    I hadn’t noticed that quirk about the nested if conditions before - we were kind of expecting that a tx wouldn’t have multiple things wrong with it. I agree it makes more sense to quit on anything that’s non-reconsiderable AlreadyHaveTx, added that as its own commit. The input shouldn’t crash anymore.

  156. marcofleon commented at 11:07 am on September 18, 2024: contributor
    0operator(): Assertion `!txdownload_impl.RecentRejectsFilter().contains(package.back()->GetWitnessHash().ToUint256())' failed.
    
    0ZT8AAAD/////ZaOjaf/y/////f39/f39odAq0KMAZWU+qOHh4eHdzMsrZTKgWuPMyytlcaBaZWVl
    1MaBaK8vczGUx/wwAUlrdzMsrZTGgWuPMyytlcaBa2WVl//////8xoFrczMsrZTEAUlrdzMsrZTGg
    2WuMrZTH/DABS/////w==
    

    txdownload_impl_crash.txt

    Child is still in reject filter? Still (slowly and steadily) working through the PR as a whole, so I’m not much help for now when it comes to actually addressing the bug…

  157. in src/test/fuzz/txdownloadman.cpp:402 in df0b8a055c outdated
    378+
    379+                    const auto& package = maybe_package->m_txns;
    380+                    // Parent is in m_lazy_recent_rejects_reconsiderable and child is in m_orphanage
    381+                    Assert(txdownload_impl.RecentRejectsReconsiderableFilter().contains(rand_tx->GetWitnessHash().ToUint256()));
    382+                    Assert(txdownload_impl.m_orphanage.HaveTx(maybe_package->m_txns.back()->GetWitnessHash()));
    383+                    // Package has not been rejected
    


    instagibbs commented at 1:20 pm on September 18, 2024:
    I think these checks on down can be pulled into CheckPackageToValidate?

    glozow commented at 2:51 pm on September 29, 2024:
    (Was my first instinct too). That requires we pass txdownload_impl to the function, and the fuzz target above doesn’t have access to that.
  158. instagibbs commented at 1:30 pm on September 18, 2024: member

    @glozow could we get a unit test for that specific behavior change :pray: @marcofleon I presume it’s simply things that “shouldn’t happen”: 0) Parent MempoolRejectedTx, put in reconsiderable filter

    1. ChildMempoolRejectedTx, put in orphanage
    2. Child MempoolRejectedTx for other reason, now in reject filter
    3. Parent ReceivedTx, found in reconsiderable filter, fetches package with fully rejected child

    I’d feel better if we also simply not return a package we should never consider, even if it “shouldn’t happen”

  159. glozow commented at 9:09 pm on September 18, 2024: member
    1. Child`MempoolRejectedTx`, put in orphanage
    
    2. Child `MempoolRejectedTx` for other reason, now in reject filter
    

    On first glance that sounds impossible, MempoolRejectedTx should have erased it from orphanage? Will look into the error

  160. glozow commented at 10:26 pm on September 18, 2024: member

    Ah, I think it was bugs in the if/else logic in MempoolRejectedTx. The flow should be:

     0if (missing inputs) {
     1  if (first time and not already rejected) {
     2    consider keeping orphan...
     3  }
     4  return
     5} else if (error A) {
     6  special A stuff
     7} else if (error B) {
     8  special B stuff
     9} else {
    10  cache rejections, etc
    11}
    

    But we (1) dropped the return early and (2) didn’t have nested ifs, so we started caching orphans in recent rejects. I’ll work on writing more unit tests. I also think it would maybe be clearer albeit more verbose to rewrite that function as a big switch statement…

  161. glozow force-pushed on Sep 18, 2024
  162. DrahtBot added the label CI failed on Sep 19, 2024
  163. glozow force-pushed on Sep 19, 2024
  164. DrahtBot removed the label CI failed on Sep 20, 2024
  165. glozow commented at 11:02 am on September 20, 2024: member
    Fixed ci
  166. in src/node/txdownloadman_impl.cpp:336 in 287510e9ea outdated
    331+                    if (rejected_parent_reconsiderable.has_value()) {
    332+                        fRejectedParents = true;
    333+                        break;
    334+                    }
    335+                }
    336+                rejected_parent_reconsiderable = parent_txid;
    


    instagibbs commented at 2:52 pm on September 20, 2024:
    I think this line got pulled out of the else if conditional. Won’t this set the value for all unique parents and reject any transaction with two unique prevout hashes from the orphanage?

    glozow commented at 7:54 pm on September 20, 2024:

    Ugh yes, a bad rebase sorry. I’ve been using a new setup and haven’t perfected my diff-viewing yet.

    I don’t understand how 0 tests failed, though 😬 gotta look into that


    instagibbs commented at 9:21 pm on September 22, 2024:

    I don’t understand how 0 tests failed, though 😬 gotta look into that

    yeah was a bit worrying to me too… glad we can add unit tests in this PR!


    glozow commented at 11:18 am on September 23, 2024:
    Added some unit tests yesterday btw, should definitely catch this in I think 2 cases.
  167. glozow force-pushed on Sep 20, 2024
  168. in src/net_processing.cpp:1 in f0b2b37dbb outdated


    l0rinc commented at 12:28 pm on September 22, 2024:

    Commit messages:

    add TxDownloadOptions bool to make deterministic TxRequestTracker

    nit: may be clearer as add TxDownloadOptions bool to make TxRequestTracker deterministic

    [refactor] ProcessInvalidTx logic to put peerman tasks at the end

    nit2: [refactor] put peerman tasks in ProcessInvalidTx logic at the end


    glozow commented at 2:27 pm on September 29, 2024:
    done
  169. in src/net_processing.cpp:571 in a69cdf87f1 outdated
    567@@ -583,53 +568,29 @@ class PeerManagerImpl final : public PeerManager
    568     bool MaybeDiscourageAndDisconnect(CNode& pnode, Peer& peer);
    569 
    570     /** Handle a transaction whose result was not MempoolAcceptResult::ResultType::VALID.
    571-     * @param[in]   maybe_add_extra_compact_tx    Whether this tx should be added to vExtraTxnForCompact.
    572+     * @param[in]   first_time_failure            Whether we should consider inserting into vExtraTxnForCompact, adding
    


    l0rinc commented at 12:34 pm on September 22, 2024:
    • how come we’re only documenting the first_time_failure parameter?
    • is there any particular reason for this weird formatting?

    glozow commented at 0:41 am on September 24, 2024:
    • Are the other parameters not self-explanatory?
    • What do you mean by weird?
  170. in src/net_processing.cpp:4259 in a69cdf87f1 outdated
    4281-            // regardless of false positives.
    4282             return;
    4283         }
    4284 
    4285+        // ReceivedTx should not be telling us to validate the tx and a package.
    4286+        Assume(!package_to_validate.has_value());
    


    l0rinc commented at 12:37 pm on September 22, 2024:
    0        Assume(!package_to_validate);
    
  171. in src/net_processing.cpp:4874 in a69cdf87f1 outdated
    4868@@ -5321,16 +4869,18 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
    4869     if (msg_type == NetMsgType::NOTFOUND) {
    4870         std::vector<CInv> vInv;
    4871         vRecv >> vInv;
    4872-        if (vInv.size() <= MAX_PEER_TX_ANNOUNCEMENTS + MAX_BLOCKS_IN_TRANSIT_PER_PEER) {
    4873-            LOCK(m_tx_download_mutex);
    4874+        std::vector<uint256> tx_invs;
    4875+        if (vInv.size() <= node::MAX_PEER_TX_ANNOUNCEMENTS + MAX_BLOCKS_IN_TRANSIT_PER_PEER) {
    4876             for (CInv &inv : vInv) {
    


    l0rinc commented at 12:45 pm on September 22, 2024:

    We could reserve the vector to the expected size to avoid resizing:

    0tx_invs.reserve(std::ranges::count_if(vInv, &CInv::IsGenTxMsg));
    1for (CInv &inv : vInv) {
    

    glozow commented at 2:30 pm on September 29, 2024:
    We don’t know the number of transaction invs ahead of time, so this could reserve a bunch of extra space that is never used. Unsure the best way to handle this - if it’s transactions, the size should generally only be a handful anyway.

    l0rinc commented at 4:41 pm on September 29, 2024:
    Isn’t that what we’d be calculating via std::ranges::count_if(vInv, &CInv::IsGenTxMsg)?

    glozow commented at 10:36 pm on October 1, 2024:
    Ah, sorry for missing that. I’m not really convinced this is more efficient overall?
  172. in src/net_processing.cpp:5898 in a69cdf87f1 outdated
    5915-                    m_txrequest.RequestedTx(pto->GetId(), gtxid.GetHash(), current_time + GETDATA_TX_INTERVAL);
    5916-                } else {
    5917-                    // We have already seen this transaction, no need to download. This is just a belt-and-suspenders, as
    5918-                    // this should already be called whenever a transaction becomes AlreadyHaveTx().
    5919-                    m_txrequest.ForgetTxHash(gtxid.GetHash());
    5920+            for (const GenTxid& gtxid : m_txdownloadman.GetRequestsToSend(pto->GetId(), current_time)) {
    


    l0rinc commented at 12:53 pm on September 22, 2024:

    same, I think we can preallocate these as well:

    0vGetData.reserve(vToDownload.size());
    1for (const CBlockIndex *pindex : vToDownload) {
    

    followed by

    0const auto& tx_requests = m_txdownloadman.GetRequestsToSend(pto->GetId(), current_time);
    1vGetData.reserve(vGetData.size() + tx_requests.size());
    2for (const GenTxid& gtxid : tx_requests) {
    
  173. in src/node/txdownloadman.h:81 in a69cdf87f1 outdated
    76+    // Move assignment
    77+    PackageToValidate& operator=(PackageToValidate&& other) {
    78+        this->m_txns = std::move(other.m_txns);
    79+        this->m_senders = std::move(other.m_senders);
    80+        return *this;
    81+    }
    


    l0rinc commented at 1:00 pm on September 22, 2024:

    We might as well simplify these:

    0    PackageToValidate(PackageToValidate&&) = default;
    1    PackageToValidate(const PackageToValidate&) = default;
    2    PackageToValidate& operator=(PackageToValidate&&) = default;
    

    glozow commented at 0:58 am on September 24, 2024:
    Considering this out of scope as it is being moved, not introduced.

    l0rinc commented at 4:39 pm on September 29, 2024:
    So when would this be in scope?
  174. in src/node/txdownloadman.h:121 in a69cdf87f1 outdated
    116+
    117+    /** Deletes all txrequest announcements and orphans for a given peer. */
    118+    void DisconnectedPeer(NodeId nodeid);
    119+
    120+    /** New inv has been received. May be added as a candidate to txrequest.
    121+     * @param[in] p2p_inv     When true, only add this announcement if we don't already have the tx.
    


    l0rinc commented at 1:01 pm on September 22, 2024:
    nit: formatting seems off

    glozow commented at 2:32 pm on September 29, 2024:
    I think you’re referring to the extra space? That’s done to leave room for other params to be added in the future, it’s more readable.
  175. in src/node/txdownloadman_impl.cpp:153 in a69cdf87f1 outdated
    148+}
    149+
    150+void TxDownloadManagerImpl::ConnectedPeer(NodeId nodeid, const TxDownloadConnectionInfo& info)
    151+{
    152+    // If already connected (shouldn't happen in practice), exit early.
    153+    if (m_peer_info.contains(nodeid)) return;
    


    l0rinc commented at 1:03 pm on September 22, 2024:
    can we log warn/error if it does? Or even better, I assume this is because of testing, can we rather move the check to the tests and assert here instead?

    glozow commented at 1:02 am on September 24, 2024:
    I prefer not to assert. My philosophy is that the interface should be simple/robust, i.e. not have a ton of assumptions that would cause the class to fall over if broken. And yes, it means we can easily fuzz that this is the case.
  176. in src/node/txdownloadman_impl.cpp:167 in a69cdf87f1 outdated
    162+    m_txrequest.DisconnectedPeer(nodeid);
    163+
    164+    if (m_peer_info.contains(nodeid)) {
    165+        if (m_peer_info.at(nodeid).m_connection_info.m_wtxid_relay) m_num_wtxid_peers -= 1;
    166+        m_peer_info.erase(nodeid);
    167+    }
    


    l0rinc commented at 1:13 pm on September 22, 2024:

    We seem to have 3 nodeid loopups here: contains(nodeid), at(nodeid), erase(nodeid). I think we can reduce it to a single iterator lookup:

    0    if (auto it = m_peer_info.find(nodeid); it != m_peer_info.end()) {
    1        if (it->second.m_connection_info.m_wtxid_relay) m_num_wtxid_peers -= 1;
    2        m_peer_info.erase(it);
    3    }
    

    glozow commented at 2:38 pm on September 29, 2024:
    Agree, changed
  177. in src/node/txdownloadman_impl.cpp:178 in a69cdf87f1 outdated
    173+    // If this is a request for the parent of an orphan, we don't drop transactions that we already have. In particular,
    174+    // we *do* want to request parents that are in m_lazy_recent_rejects_reconsiderable, since they can be CPFP'd.
    175+    if (p2p_inv && AlreadyHaveTx(gtxid, /*include_reconsiderable=*/true)) return true;
    176+
    177+    if (m_peer_info.count(peer) == 0) return false;
    178+    const auto& info = m_peer_info.at(peer).m_connection_info;
    


    l0rinc commented at 1:19 pm on September 22, 2024:

    Similarly we can avoid two lookups here:

    0    auto it = m_peer_info.find(peer);
    1    if (it == m_peer_info.end()) return false;
    2    const auto& info = it->second.m_connection_info;
    

    glozow commented at 2:40 pm on September 29, 2024:
    done
  178. in src/node/txdownloadman_impl.cpp:277 in a69cdf87f1 outdated
    270+    std::shuffle(tx_indices.begin(), tx_indices.end(), m_opts.m_rng);
    271+
    272+    for (const auto index : tx_indices) {
    273+        // If we already tried a package and failed for any reason, the combined hash was
    274+        // cached in m_lazy_recent_rejects_reconsiderable.
    275+        const auto [child_tx, child_sender] = cpfp_candidates_different_peer.at(index);
    


    l0rinc commented at 1:43 pm on September 22, 2024:

    What’s the rationale behind shuffling the indices instead of the values? That requires an O(n) copy, and the elements are accessed randomly, which could impact locality. Unless I’m missing something (likely at this stage, that’s why I’m asking), wouldn’t shuffling the values directly achieve the same outcome while avoiding the copy and potentially improving cache locality during iteration?

     0    auto cpfp_candidates_different_peer = m_orphanage.GetChildrenFromDifferentPeer(ptx, nodeid);
     1    // Find the first 1p1c that hasn't already been rejected. We randomize the order to not
     2    // create a bias that attackers can use to delay package acceptance.
     3    //
     4    std::shuffle(cpfp_candidates_different_peer.begin(), cpfp_candidates_different_peer.end(), m_opts.m_rng);
     5    for (const auto& [child_tx, child_sender] : cpfp_candidates_different_peer) {
     6        // If we already tried a package and failed for any reason, the combined hash was
     7        // cached in m_lazy_recent_rejects_reconsiderable.
     8        Package maybe_cpfp_package{ptx, child_tx};
     9        if (!RecentRejectsReconsiderableFilter().contains(GetPackageHash(maybe_cpfp_package))) {
    10            return PackageToValidate{ptx, child_tx, nodeid, child_sender};
    11        }
    12    }
    13    return std::nullopt;
    

    glozow commented at 0:57 am on September 24, 2024:

    Considering this out of scope as it is being moved, not introduced.

    Also, this is intended for deletion in #28031.

  179. in src/node/txdownloadman_impl.cpp:327 in a69cdf87f1 outdated
    316+            for (const CTxIn& txin : tx.vin) {
    317+                // We start with all parents, and then remove duplicates below.
    318+                unique_parents.push_back(txin.prevout.hash);
    319+            }
    320+            std::sort(unique_parents.begin(), unique_parents.end());
    321+            unique_parents.erase(std::unique(unique_parents.begin(), unique_parents.end()), unique_parents.end());
    


    l0rinc commented at 1:56 pm on September 22, 2024:

    sorting + erasing duplicates indicates to me that we may be using the wrong datastructure:

    0    std::set<uint256> unique_parents;
    1...
    2    // Deduplicate parent txids, so that we don't have to loop over
    3    // the same parent txid more than once down below.
    4    for (const CTxIn& txin : tx.vin) {
    5        // We start with all parents, and then remove duplicates below.
    6        unique_parents.insert(txin.prevout.hash);
    7    }
    

    (std::vector seems to have O(n) inserts + O(nlogn) sort + O(n) deduplication, std::set has simply O(nlogn) sorted inserts)


    glozow commented at 0:57 am on September 24, 2024:
    Considering this out of scope as it is being moved, not introduced.
  180. in src/test/txdownload_tests.cpp:176 in a69cdf87f1 outdated
    171+        BOOST_CHECK(!p1_package.has_value());
    172+
    173+        // Child missing inputs, should be added to orphanage.
    174+        const auto& [c_keep, c_unique_txids, c_package] = txdownload_impl.MempoolRejectedTx(ptx_child, state_orphan, nodeid, /*first_time_failure=*/true);
    175+        BOOST_CHECK(c_keep);
    176+        BOOST_CHECK(c_unique_txids == vec_parent_txids);
    


    l0rinc commented at 2:03 pm on September 22, 2024:

    While BOOST_CHECK_EQUAL_COLLECTIONS is more verbose, it produces better failure messages:

    0        BOOST_CHECK_EQUAL_COLLECTIONS(c_unique_txids.begin(), c_unique_txids.end(), vec_parent_txids.begin(), vec_parent_txids.end());
    
  181. in src/test/txdownload_tests.cpp:171 in a69cdf87f1 outdated
    166+
    167+        // Parent reconsiderable failure
    168+        const auto& [p1_keep, p1_unique_txids, p1_package] = txdownload_impl.MempoolRejectedTx(ptx_parent_1, state_reconsiderable, nodeid, /*first_time_failure=*/true);
    169+        BOOST_CHECK(p1_keep);
    170+        BOOST_CHECK(p1_unique_txids.empty());
    171+        BOOST_CHECK(!p1_package.has_value());
    


    l0rinc commented at 2:04 pm on September 22, 2024:
    0        BOOST_CHECK(!p1_package);
    
  182. in src/test/txdownload_tests.cpp:99 in a69cdf87f1 outdated
    65+    {TxValidationResult::TX_NO_MEMPOOL,              {/*txid_rejects*/0, /*wtxid_rejects*/1, /*txid_recon*/0, /*wtxid_recon*/0, /*keep*/1, /*txid_inv*/0, /*wtxid_inv*/1}},
    66+    {TxValidationResult::TX_RECONSIDERABLE,          {/*txid_rejects*/0, /*wtxid_rejects*/0, /*txid_recon*/0, /*wtxid_recon*/1, /*keep*/1, /*txid_inv*/0, /*wtxid_inv*/1}},
    67+    {TxValidationResult::TX_UNKNOWN,                 {/*txid_rejects*/0, /*wtxid_rejects*/1, /*txid_recon*/0, /*wtxid_recon*/0, /*keep*/1, /*txid_inv*/0, /*wtxid_inv*/1}},
    68+};
    69+
    70+static std::vector<CTransactionRef> CreateTransactionChain(size_t num_txns, bool segwit)
    


    l0rinc commented at 2:06 pm on September 22, 2024:
    nit: we seem to be always calling this with num_txns == 1

    glozow commented at 9:54 pm on October 1, 2024:
    edited to no longer be for a chain
  183. in src/test/txdownload_tests.cpp:72 in a69cdf87f1 outdated
    62+    {TxValidationResult::TX_WITNESS_STRIPPED,        {/*txid_rejects*/0, /*wtxid_rejects*/0, /*txid_recon*/0, /*wtxid_recon*/0, /*keep*/0, /*txid_inv*/0, /*wtxid_inv*/0}},
    63+    {TxValidationResult::TX_CONFLICT,                {/*txid_rejects*/0, /*wtxid_rejects*/1, /*txid_recon*/0, /*wtxid_recon*/0, /*keep*/1, /*txid_inv*/0, /*wtxid_inv*/1}},
    64+    {TxValidationResult::TX_MEMPOOL_POLICY,          {/*txid_rejects*/0, /*wtxid_rejects*/1, /*txid_recon*/0, /*wtxid_recon*/0, /*keep*/1, /*txid_inv*/0, /*wtxid_inv*/1}},
    65+    {TxValidationResult::TX_NO_MEMPOOL,              {/*txid_rejects*/0, /*wtxid_rejects*/1, /*txid_recon*/0, /*wtxid_recon*/0, /*keep*/1, /*txid_inv*/0, /*wtxid_inv*/1}},
    66+    {TxValidationResult::TX_RECONSIDERABLE,          {/*txid_rejects*/0, /*wtxid_rejects*/0, /*txid_recon*/0, /*wtxid_recon*/1, /*keep*/1, /*txid_inv*/0, /*wtxid_inv*/1}},
    67+    {TxValidationResult::TX_UNKNOWN,                 {/*txid_rejects*/0, /*wtxid_rejects*/1, /*txid_recon*/0, /*wtxid_recon*/0, /*keep*/1, /*txid_inv*/0, /*wtxid_inv*/1}},
    


    l0rinc commented at 2:09 pm on September 22, 2024:

    Alternatively, to avoid repetition and noise (labels AND data) we could group it slightly differently:

     0    //                                                txid_rejects, wtxid_rejects, txid_recon, wtxid_recon, keep, txid_inv, wtxid_inv
     1    {TxValidationResult::TX_CONSENSUS,               {0,            1,             0,          0,           1,    0,        1}},
     2    {TxValidationResult::TX_RECENT_CONSENSUS_CHANGE, {0,            1,             0,          0,           1,    0,        1}},
     3    {TxValidationResult::TX_INPUTS_NOT_STANDARD,     {1,            1,             0,          0,           1,    1,        1}},
     4    {TxValidationResult::TX_NOT_STANDARD,            {0,            1,             0,          0,           1,    0,        1}},
     5    {TxValidationResult::TX_MISSING_INPUTS,          {0,            0,             0,          0,           1,    0,        1}},
     6    {TxValidationResult::TX_PREMATURE_SPEND,         {0,            1,             0,          0,           1,    0,        1}},
     7    {TxValidationResult::TX_WITNESS_MUTATED,         {0,            1,             0,          0,           1,    0,        1}},
     8    {TxValidationResult::TX_WITNESS_STRIPPED,        {0,            0,             0,          0,           0,    0,        0}},
     9    {TxValidationResult::TX_CONFLICT,                {0,            1,             0,          0,           1,    0,        1}},
    10    {TxValidationResult::TX_MEMPOOL_POLICY,          {0,            1,             0,          0,           1,    0,        1}},
    11    {TxValidationResult::TX_NO_MEMPOOL,              {0,            1,             0,          0,           1,    0,        1}},
    12    {TxValidationResult::TX_RECONSIDERABLE,          {0,            0,             0,          1,           1,    0,        1}},
    13    {TxValidationResult::TX_UNKNOWN,                 {0,            1,             0,          0,           1,    0,        1}},
    

    glozow commented at 10:02 pm on October 1, 2024:
    edited somewhat. I like the annotations there but only did first row.
  184. in src/test/fuzz/txdownloadman_one_honest_peer.cpp:512 in a69cdf87f1 outdated
    507+                }
    508+                ++counter;
    509+            }
    510+        }
    511+
    512+        // Skip time by 100-300ms
    


    l0rinc commented at 2:13 pm on September 22, 2024:
    ms or μs?

    glozow commented at 1:06 am on September 24, 2024:
    ms

    l0rinc commented at 4:28 pm on September 29, 2024:
    In that case please consider using std::chrono::milliseconds instead of multiplying micros by 1000 - making the comment redundant.

    glozow commented at 10:26 pm on October 1, 2024:
    done
  185. in src/test/fuzz/txdownloadman_one_honest_peer.cpp:437 in a69cdf87f1 outdated
    434+                        break;
    435+                    }
    436+                }
    437+
    438+                // The peer may also send some unsolicited message. Simulate a ProcessMessage.
    439+                // Ensure the good peer sends the inv within the first 1000 rounds.
    


    l0rinc commented at 2:16 pm on September 22, 2024:
    since counter starts from 0, aren’t we sending within the first 1001 rounds?

    glozow commented at 2:58 pm on September 29, 2024:
    Right, changed
  186. in src/test/fuzz/txdownloadman_one_honest_peer.cpp:327 in a69cdf87f1 outdated
    322+        all_peers.emplace_back(RandomPeer(fuzzed_data_provider, i, /*preferred=*/false, /*force_good=*/good));
    323+    }
    324+
    325+    // Indexes for random peer iteration
    326+    std::vector<size_t> indexes;
    327+    indexes.resize(all_peers.size());
    


    l0rinc commented at 2:21 pm on September 22, 2024:

    we can simplify this to either:

    0std::vector<size_t> indexes(all_peers.size());
    

    or shuffle all_peers directly (unless I’m missing something, as mentioned in a previous comment)


    glozow commented at 10:25 pm on October 1, 2024:
    More efficient to shuffle ints than objects. I’ve changed to use the ctor with the size.
  187. in src/test/fuzz/txdownloadman_one_honest_peer.cpp:248 in a69cdf87f1 outdated
    244+}
    245+
    246+static CTransactionRef GetCorrectTx(const std::map<uint256, std::pair<CTransactionRef, CTransactionRef>> txmap, const GenTxid& gtxid)
    247+{
    248+    auto tx_it = txmap.find(gtxid.GetHash());
    249+    if (tx_it != txmap.end()) {
    


    l0rinc commented at 2:22 pm on September 22, 2024:

    nit: we can narrow the scope of the iterator:

    0    if (auto tx_it = txmap.find(gtxid.GetHash()); tx_it != txmap.end()) {
    

    glozow commented at 3:01 pm on September 29, 2024:
    Scope seems to be about the same? There isn’t anything happening after this section.

    l0rinc commented at 4:29 pm on September 29, 2024:
    Exactly, that’s what we’d be signalling by reducing the scope
  188. in src/test/fuzz/txdownloadman_one_honest_peer.cpp:242 in a69cdf87f1 outdated
    237+
    238+        // The first tx in the pair needs to match by wtxid.
    239+        TRANSACTIONS.emplace(target_wtxid, std::make_pair(correct_tx, malleated_tx));
    240+        TRANSACTIONS.emplace(target_txid, std::make_pair(correct_tx, correct_tx));
    241+
    242+        TRANSACTIONS.emplace(malleated_wtxid, std::make_pair(malleated_tx, stripped_tx));
    


    l0rinc commented at 2:29 pm on September 22, 2024:

    we can modernize these slightly:

    0        TRANSACTIONS.try_emplace(target_wtxid, correct_tx, malleated_tx);
    1        TRANSACTIONS.try_emplace(target_txid, correct_tx, correct_tx);
    2
    3        TRANSACTIONS.try_emplace(malleated_wtxid, malleated_tx, stripped_tx);
    

    glozow commented at 10:23 pm on October 1, 2024:
    Thanks, done
  189. in src/test/fuzz/txdownloadman_one_honest_peer.cpp:302 in a69cdf87f1 outdated
    298+    // Decide the target transaction. Disallow selecting a witness-stripped or non-segwit
    299+    // transaction because it can lead to false negatives. This test may mark a same-txid
    300+    // transaction as invalid, and we don't want that to cause rejection of the target tx.
    301+    const auto& target_tx = GetRandomTx(TRANSACTIONS, fuzzed_data_provider, /*correct_only=*/true);
    302+    Assert(target_tx != nullptr);
    303+    Assert(target_tx->HasWitness());
    


    l0rinc commented at 2:30 pm on September 22, 2024:
    what’s the reason for Assert in tests instead of assert (as both are used here)?

    glozow commented at 10:24 pm on October 1, 2024:
    Unsure, it’s just a habit I guess.
  190. in src/test/fuzz/txdownloadman_one_honest_peer.cpp:274 in a69cdf87f1 outdated
    269+}
    270+static CTransactionRef GetRandomTx(const std::map<uint256, std::pair<CTransactionRef, CTransactionRef>> txmap, FuzzedDataProvider& fuzzed_data_provider, bool correct_only = false)
    271+{
    272+    auto it = txmap.begin();
    273+    // Jump forward a random number of times.
    274+    for (size_t i = 0; i <= fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, txmap.size() - 1); ++i) it++;
    


    l0rinc commented at 2:31 pm on September 22, 2024:
    • aren’t we jumping one more time than needed (<=)? This way we can’t select the first value, right?
    • isn’t fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, txmap.size() - 1 reevaluated on every iteration? What’s the advantage of that over generating the limit a single time, e.g.:
    0    if (txmap.empty()) return nullptr;
    1    size_t rand_index = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, txmap.size() - 1);
    2    auto it = std::next(txmap.begin(), rand_index);
    3    return correct_only || fuzzed_data_provider.ConsumeBool() ? it->second.first : it->second.second;
    

    glozow commented at 2:58 pm on September 29, 2024:
    Changed to something like this
  191. in src/test/fuzz/txdownloadman.cpp:75 in a69cdf87f1 outdated
    70+}
    71+void initialize()
    72+{
    73+    static const auto testing_setup = MakeNoLogFileContext<const TestingSetup>();
    74+    g_setup = testing_setup.get();
    75+    for (uint32_t i = 0; i < uint32_t{NUM_COINS}; ++i) {
    


    l0rinc commented at 2:42 pm on September 22, 2024:
    Could we init it as constexpr uint32_t NUM_COINS{50}; instead?

    glozow commented at 2:48 pm on September 29, 2024:
    see L29, that’s already done?

    l0rinc commented at 4:55 pm on September 29, 2024:
    I was referring to getting rid of the cast, but not important, please resolve these.
  192. in src/test/fuzz/txdownloadman_one_honest_peer.cpp:157 in a69cdf87f1 outdated
    152+            m_getdata_queue.push_back(request);
    153+            m_getdata_queue.pop_front();
    154+            return {PossibleResponse::NOTHING, std::nullopt};
    155+        }
    156+
    157+        m_getdata_queue.pop_front();
    


    l0rinc commented at 3:45 pm on September 22, 2024:

    seems we’re always popping, and the queue is never empty, might as well pop once after we’re getting the front value (and before we’re potentially pushing):

     0// Process up to one message from the front of m_getdata_queue. The size of m_getdata_queue may
     1// not change, as DEFER may just push it to the back again.
     2std::pair<PossibleResponse, std::optional<GenTxid>> ProcessOneMessage(FuzzedDataProvider& fuzzed_data_provider)
     3{
     4    // Nothing to do if no messages to process
     5    if (m_getdata_queue.empty()) return {PossibleResponse::NOTHING, std::nullopt};
     6
     7    auto request{m_getdata_queue.front()};
     8    m_getdata_queue.pop_front();
     9
    10    auto response{m_good ? PossibleResponse::TX_REAL : fuzzed_data_provider.PickValueInArray(NON_GOOD_RESPONSES)};
    11    if (response == PossibleResponse::DEFER) {
    12        // Move the request from the front to the back of the queue and return nothing.
    13        // This simulates responding out of order and/or stalling.
    14        m_getdata_queue.emplace_back(std::move(request));
    15        return {PossibleResponse::NOTHING, std::nullopt};
    16    } else {
    17        return {response, request};
    18    }
    19}
    

    note: changed to emplace_back and used std::move to avoid copies and merged response inits. Also note that deleting before pushing is a tiny bit better memory-wise (but I doubt it matters here)

  193. in src/node/txdownloadman_impl.h:108 in a69cdf87f1 outdated
    103+    }
    104+
    105+    /*
    106+     * Filter for transactions that have been recently confirmed.
    107+     * We use this to avoid requesting transactions that have already been
    108+     * confirnmed.
    


    l0rinc commented at 4:09 pm on September 22, 2024:
    0     * confirmed.
    
  194. in src/node/txdownloadman_impl.cpp:329 in a69cdf87f1 outdated
    318+                unique_parents.push_back(txin.prevout.hash);
    319+            }
    320+            std::sort(unique_parents.begin(), unique_parents.end());
    321+            unique_parents.erase(std::unique(unique_parents.begin(), unique_parents.end()), unique_parents.end());
    322+
    323+            // Distinguish between parents in m_lazy_recent_rejects and m_lazy_recent_rejects_reconsiderable.
    


    l0rinc commented at 4:19 pm on September 22, 2024:
    m_lazy_recent_rejects_reconsiderable is usually accessed through RecentRejectsReconsiderableFilter, might make sense to update the comments

    glozow commented at 0:58 am on September 24, 2024:
    Considering this out of scope as it is being moved, not introduced.

    l0rinc commented at 4:31 pm on September 29, 2024:
    There are many such cases, are none of them related to this change?

    glozow commented at 10:34 pm on October 1, 2024:
    This is part of a block of code that was moved from PeerManagerImpl. The members still have the same name, so I don’t think this is incorrect.
  195. in src/net_processing.cpp:873 in 85edaf2556 outdated
    903-     */
    904-    std::unique_ptr<CRollingBloomFilter> m_lazy_recent_rejects GUARDED_BY(m_tx_download_mutex){nullptr};
    905 
    906     CRollingBloomFilter& RecentRejectsFilter() EXCLUSIVE_LOCKS_REQUIRED(m_tx_download_mutex)
    907     {
    908         AssertLockHeld(m_tx_download_mutex);
    


    l0rinc commented at 4:33 pm on September 22, 2024:
    how come we’re removing all locks here?
  196. in src/net_processing.cpp:1600 in 85edaf2556 outdated
    1594@@ -1685,6 +1595,8 @@ void PeerManagerImpl::AddTxAnnouncement(const CNode& node, const GenTxid& gtxid,
    1595     AssertLockHeld(::cs_main); // for State
    1596     AssertLockHeld(m_tx_download_mutex); // For m_txrequest
    1597     NodeId nodeid = node.GetId();
    1598+
    1599+    auto& m_txrequest = m_txdownloadman.GetTxRequestRef();
    1600     if (!node.HasPermission(NetPermissionFlags::Relay) && m_txrequest.Count(nodeid) >= MAX_PEER_TX_ANNOUNCEMENTS) {
    


    l0rinc commented at 4:50 pm on September 22, 2024:
    what’s the reason for changing m_txrequest.Count(nodeid) to m_txdownloadman.GetTxRequestRef().Count in the init but not here (85edaf2556c9ce531573dc3dee8fff2f7b4d9abf)
  197. glozow force-pushed on Sep 22, 2024
  198. glozow force-pushed on Sep 22, 2024
  199. DrahtBot added the label CI failed on Sep 22, 2024
  200. DrahtBot commented at 10:23 pm on September 22, 2024: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/30495116864

    Make sure to run all tests locally, according to the documentation.

    The failure may happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  201. DrahtBot removed the label CI failed on Sep 22, 2024
  202. in src/node/txdownloadman.h:91 in b543dc083d outdated
    86+                         m_txns.front()->GetHash().ToString(),
    87+                         m_txns.front()->GetWitnessHash().ToString(),
    88+                         m_senders.front(),
    89+                         m_txns.back()->GetHash().ToString(),
    90+                         m_txns.back()->GetWitnessHash().ToString(),
    91+                         m_senders.back());
    


    l0rinc commented at 9:18 am on September 23, 2024:

    nit: we could avoid the repetition via a lambda:

    0        auto format_tx{[&](const CTransactionRef& tx, NodeId sender) {
    1            return strprintf("%s (wtxid=%s, sender=%d)", tx->GetHash().ToString(), tx->GetWitnessHash().ToString(), sender);
    2        }};
    3        return strprintf("parent %s + child %s", format_tx(m_txns.front(), m_senders.front()), format_tx(m_txns.back(), m_senders.back()));
    

    glozow commented at 0:58 am on September 24, 2024:
    Considering this out of scope as it is being moved, not introduced.
  203. in src/node/txdownloadman_impl.cpp:155 in b543dc083d outdated
    150+void TxDownloadManagerImpl::ConnectedPeer(NodeId nodeid, const TxDownloadConnectionInfo& info)
    151+{
    152+    // If already connected (shouldn't happen in practice), exit early.
    153+    if (m_peer_info.contains(nodeid)) return;
    154+
    155+    m_peer_info.emplace(nodeid, PeerInfo(info));
    


    l0rinc commented at 9:22 am on September 23, 2024:

    Can be modernized to:

    0    m_peer_info.try_emplace(nodeid, info);
    

    glozow commented at 2:36 pm on September 29, 2024:
    Sure, done
  204. in src/net_processing.cpp:3941 in b543dc083d outdated
    3941-                if (!fAlreadyHave && !m_chainman.IsInitialBlockDownload()) {
    3942-                    AddTxAnnouncement(pfrom, gtxid, current_time);
    3943+
    3944+                if (!m_chainman.IsInitialBlockDownload()) {
    3945+                    const bool fAlreadyHave{m_txdownloadman.AddTxAnnouncement(pfrom.GetId(), gtxid, current_time, /*p2p_inv=*/true)};
    3946+                    LogDebug(BCLog::NET, "got inv: %s  %s peer=%d\n", inv.ToString(), fAlreadyHave ? "have" : "new", pfrom.GetId());
    


    l0rinc commented at 9:27 am on September 23, 2024:

    super-nit:

    0                    LogDebug(BCLog::NET, "got inv: %s %s peer=%d\n", inv.ToString(), fAlreadyHave ? "have" : "new", pfrom.GetId());
    

    glozow commented at 0:42 am on September 24, 2024:
    The string isn’t changed in this PR, marking as resolved. Can be done in a followup.
  205. in src/net_processing.cpp:5002 in b543dc083d outdated
    4998@@ -5449,7 +4999,7 @@ bool PeerManagerImpl::ProcessMessages(CNode* pfrom, std::atomic<bool>& interrupt
    4999         //  the extra work may not be noticed, possibly resulting in an
    5000         //  unnecessary 100ms delay)
    5001         LOCK(m_tx_download_mutex);
    5002-        if (m_orphanage.HaveTxToReconsider(peer->m_id)) fMoreWork = true;
    5003+        if (m_txdownloadman.HaveMoreWork(peer->m_id)) fMoreWork = true;
    


    l0rinc commented at 9:34 am on September 23, 2024:

    do we need to call the const HaveMoreWork when fMoreWork was already true? If not, we may even avoid the the lock:

    0        if (!fMoreWork) {
    1            LOCK(m_tx_download_mutex);
    2            fMoreWork = m_txdownloadman.HaveMoreWork(peer->m_id);
    3        }
    
  206. in src/node/txdownloadman_impl.cpp:80 in b543dc083d outdated
    75+{
    76+    return m_impl->GetTxToReconsider(nodeid);
    77+}
    78+void TxDownloadManager::CheckIsEmpty() const
    79+{
    80+    return m_impl->CheckIsEmpty();
    


    l0rinc commented at 9:42 am on September 23, 2024:
    is it deliberate to return in a void method here?

    glozow commented at 2:33 pm on September 29, 2024:
    Nope - fixed, thanks
  207. in src/node/txdownloadman_impl.cpp:147 in b543dc083d outdated
    142+
    143+    if (include_reconsiderable && RecentRejectsReconsiderableFilter().contains(hash)) return true;
    144+
    145+    if (RecentConfirmedTransactionsFilter().contains(hash)) return true;
    146+
    147+    return RecentRejectsFilter().contains(hash) || m_opts.m_mempool.exists(gtxid);
    


    l0rinc commented at 9:45 am on September 23, 2024:

    Seems to me we could simplify to a single return:

    0    return (include_reconsiderable && RecentRejectsReconsiderableFilter().contains(hash)) 
    1           || RecentConfirmedTransactionsFilter().contains(hash) 
    2           || RecentRejectsFilter().contains(hash) 
    3           || m_opts.m_mempool.exists(gtxid);
    

    glozow commented at 1:00 am on September 24, 2024:
    Considering this out of scope as it is being moved, not introduced.
  208. in src/node/txdownloadman_impl.cpp:507 in b543dc083d outdated
    502+                 txid.ToString(), wtxid.ToString());
    503+        return std::make_pair(false, Find1P1CPackage(ptx, nodeid));
    504+    }
    505+
    506+
    507+    return std::make_pair(true, std::nullopt);
    


    l0rinc commented at 9:49 am on September 23, 2024:

    we should be able to modernize these:

    0        return {false, Find1P1CPackage(ptx, nodeid)};
    1    }
    2
    3
    4    return {true, std::nullopt};
    

    glozow commented at 2:45 pm on September 29, 2024:
    done
  209. in src/node/txdownloadman_impl.h:61 in b543dc083d outdated
    56+     * validation no matter the witness, we may add the txid of such
    57+     * transaction to the filter as well. This can be helpful when
    58+     * communicating with txid-relay peers or if we were to otherwise fetch a
    59+     * transaction via txid (eg in our orphan handling).
    60+     *
    61+     * Memory used: 1.3 MB
    


    l0rinc commented at 9:50 am on September 23, 2024:
    do we know if this is still the case?

    glozow commented at 2:43 pm on September 29, 2024:
    Good question. See the comment on src/common/bloom.h for the usage formula, AFAIK it has not changed.
  210. in src/test/fuzz/txdownloadman.cpp:395 in b543dc083d outdated
    390+            [&] {
    391+                txdownload_impl.ReceivedNotFound(rand_peer, {rand_tx->GetWitnessHash()});
    392+            },
    393+            [&] {
    394+                const bool expect_work{txdownload_impl.HaveMoreWork(rand_peer)};
    395+                const auto ptx = txdownload_impl.GetTxToReconsider(rand_peer);
    


    l0rinc commented at 9:53 am on September 23, 2024:
    nit: previous line used brace initialization, this one uses assignment

    glozow commented at 2:51 pm on September 29, 2024:
    changed
  211. in src/test/txdownload_tests.cpp:81 in b543dc083d outdated
    76+                                bool expect_orphan, bool expect_keep, unsigned int expected_parents)
    77+{
    78+    // Missing inputs can never result in a PackageToValidate.
    79+    if (ret.m_package_to_validate.has_value()) {
    80+        err_msg = strprintf("returned a PackageToValidate on missing inputs");
    81+        return false;
    


    l0rinc commented at 9:56 am on September 23, 2024:
    so we’re setting an input param and returning whether we’ve modified it, right? Could we modernize it to return an std::optional instead?

    glozow commented at 3:00 pm on September 29, 2024:
    Yeah this is so we can have BOOST_CHECK_MESSAGE easily check and print the error. IIRC I had trouble getting the std::optional way to work.

    l0rinc commented at 4:33 pm on September 29, 2024:
  212. in src/node/txdownloadman.h:27 in b543dc083d outdated
    22+namespace node {
    23+class TxDownloadManagerImpl;
    24+
    25+/** Maximum number of in-flight transaction requests from a peer. It is not a hard limit, but the threshold at which
    26+ *  point the OVERLOADED_PEER_TX_DELAY kicks in. */
    27+static constexpr int32_t MAX_PEER_TX_REQUEST_IN_FLIGHT = 100;
    


    l0rinc commented at 10:06 am on September 23, 2024:
    nit: brace inits vs assignments

    glozow commented at 0:58 am on September 24, 2024:
    Considering this out of scope as it is being moved, not introduced.
  213. in src/node/txdownloadman_impl.cpp:87 in b543dc083d outdated
    82+void TxDownloadManager::CheckIsEmpty(NodeId nodeid) const
    83+{
    84+    return m_impl->CheckIsEmpty(nodeid);
    85+}
    86+
    87+// TxDownloadManagerImpl
    


    l0rinc commented at 10:11 am on September 23, 2024:
    what’s the role of this comment, is it just a delimiter?

    glozow commented at 2:33 pm on September 29, 2024:
    Yes
  214. in src/node/txdownloadman_impl.cpp:141 in b543dc083d outdated
    136+        //
    137+        // While we won't query by txid, we can try to "guess" what the wtxid is based on the txid.
    138+        // A non-segwit transaction's txid == wtxid. Query this txid "casted" to a wtxid. This will
    139+        // help us find non-segwit transactions, saving bandwidth, and should have no false positives.
    140+        if (m_orphanage.HaveTx(Wtxid::FromUint256(hash))) return true;
    141+    }
    


    l0rinc commented at 10:16 am on September 23, 2024:

    I don’t understand the purpose of this, basically if orphanage has the hash transaction, we’re returning true, regardless of whether IsWtxid or not.

    0    if (m_orphanage.HaveTx(Wtxid::FromUint256(hash))) return true;
    

    glozow commented at 1:00 am on September 24, 2024:
    Considering this out of scope as it is being moved, not introduced.

    glozow commented at 2:34 pm on September 29, 2024:
    You may find #30000 discussion interesting
  215. l0rinc commented at 10:55 am on September 23, 2024: contributor

    Concept ACK

    I went over the code a few times and tried to document my immediate observations. I understand if you think some comments are outside the scope of this PR - I’ll provide a higher-level review later, once I understand the problem better. I still need a few iterations to be able to zoom out and comment meaningfully, for now I only left nits about implementation specifics, modernization attempts, potential off-by-one errors, readability suggestions, etc.

      0Index: src/node/txdownloadman_impl.h
      1<+>UTF-8
      2===================================================================
      3diff --git a/src/node/txdownloadman_impl.h b/src/node/txdownloadman_impl.h
      4--- a/src/node/txdownloadman_impl.h	(revision b543dc083ddbf43bb172b60ddb90609a600c86b5)
      5+++ b/src/node/txdownloadman_impl.h	(date 1727088265499)
      6@@ -10,14 +10,15 @@
      7 #include <consensus/validation.h>
      8 #include <kernel/chain.h>
      9 #include <net.h>
     10-#include <primitives/transaction.h>
     11 #include <policy/packages.h>
     12+#include <primitives/transaction.h>
     13 #include <txorphanage.h>
     14 #include <txrequest.h>
     15 
     16 class CTxMemPool;
     17 namespace node {
     18-class TxDownloadManagerImpl {
     19+class TxDownloadManagerImpl
     20+{
     21 public:
     22     TxDownloadOptions m_opts;
     23 
     24@@ -105,7 +106,7 @@
     25     /*
     26      * Filter for transactions that have been recently confirmed.
     27      * We use this to avoid requesting transactions that have already been
     28-     * confirnmed.
     29+     * confirmed.
     30      *
     31      * Blocks don't typically have more than 4000 transactions, so this should
     32      * be at least six blocks (~1 hr) worth of transactions that we can store,
     33Index: src/node/txdownloadman_impl.cpp
     34<+>UTF-8
     35===================================================================
     36diff --git a/src/node/txdownloadman_impl.cpp b/src/node/txdownloadman_impl.cpp
     37--- a/src/node/txdownloadman_impl.cpp	(revision b543dc083ddbf43bb172b60ddb90609a600c86b5)
     38+++ b/src/node/txdownloadman_impl.cpp	(date 1727088443418)
     39@@ -2,8 +2,8 @@
     40 // Distributed under the MIT software license, see the accompanying
     41 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
     42 
     43-#include <node/txdownloadman_impl.h>
     44 #include <node/txdownloadman.h>
     45+#include <node/txdownloadman_impl.h>
     46 
     47 #include <chain.h>
     48 #include <consensus/validation.h>
     49@@ -84,7 +84,8 @@
     50     return m_impl->CheckIsEmpty(nodeid);
     51 }
     52 
     53-// TxDownloadManagerImpl
     54+// ---------
     55+
     56 void TxDownloadManagerImpl::ActiveTipChange()
     57 {
     58     RecentRejectsFilter().reset();
     59@@ -121,30 +122,9 @@
     60 bool TxDownloadManagerImpl::AlreadyHaveTx(const GenTxid& gtxid, bool include_reconsiderable)
     61 {
     62     const uint256& hash = gtxid.GetHash();
     63-
     64-    if (gtxid.IsWtxid()) {
     65-        // Normal query by wtxid.
     66-        if (m_orphanage.HaveTx(Wtxid::FromUint256(hash))) return true;
     67-    } else {
     68-        // Never query by txid: it is possible that the transaction in the orphanage has the same
     69-        // txid but a different witness, which would give us a false positive result. If we decided
     70-        // not to request the transaction based on this result, an attacker could prevent us from
     71-        // downloading a transaction by intentionally creating a malleated version of it.  While
     72-        // only one (or none!) of these transactions can ultimately be confirmed, we have no way of
     73-        // discerning which one that is, so the orphanage can store multiple transactions with the
     74-        // same txid.
     75-        //
     76-        // While we won't query by txid, we can try to "guess" what the wtxid is based on the txid.
     77-        // A non-segwit transaction's txid == wtxid. Query this txid "casted" to a wtxid. This will
     78-        // help us find non-segwit transactions, saving bandwidth, and should have no false positives.
     79-        if (m_orphanage.HaveTx(Wtxid::FromUint256(hash))) return true;
     80-    }
     81-
     82-    if (include_reconsiderable && RecentRejectsReconsiderableFilter().contains(hash)) return true;
     83+    if (m_orphanage.HaveTx(Wtxid::FromUint256(hash))) return true;
     84 
     85-    if (RecentConfirmedTransactionsFilter().contains(hash)) return true;
     86-
     87-    return RecentRejectsFilter().contains(hash) || m_opts.m_mempool.exists(gtxid);
     88+    return (include_reconsiderable && RecentRejectsReconsiderableFilter().contains(hash)) || RecentConfirmedTransactionsFilter().contains(hash) || RecentRejectsFilter().contains(hash) || m_opts.m_mempool.exists(gtxid);
     89 }
     90 
     91 void TxDownloadManagerImpl::ConnectedPeer(NodeId nodeid, const TxDownloadConnectionInfo& info)
     92@@ -152,7 +132,7 @@
     93     // If already connected (shouldn't happen in practice), exit early.
     94     if (m_peer_info.contains(nodeid)) return;
     95 
     96-    m_peer_info.emplace(nodeid, PeerInfo(info));
     97+    m_peer_info.try_emplace(nodeid, info);
     98     if (info.m_wtxid_relay) m_num_wtxid_peers += 1;
     99 }
    100 
    101@@ -161,9 +141,9 @@
    102     m_orphanage.EraseForPeer(nodeid);
    103     m_txrequest.DisconnectedPeer(nodeid);
    104 
    105-    if (m_peer_info.contains(nodeid)) {
    106-        if (m_peer_info.at(nodeid).m_connection_info.m_wtxid_relay) m_num_wtxid_peers -= 1;
    107-        m_peer_info.erase(nodeid);
    108+    if (auto it = m_peer_info.find(nodeid); it != m_peer_info.end()) {
    109+        if (it->second.m_connection_info.m_wtxid_relay) m_num_wtxid_peers -= 1;
    110+        m_peer_info.erase(it);
    111     }
    112 }
    113 
    114@@ -171,11 +151,12 @@
    115 {
    116     // If this is an inv received from a peer and we already have it, we can drop it.
    117     // If this is a request for the parent of an orphan, we don't drop transactions that we already have. In particular,
    118-    // we *do* want to request parents that are in m_lazy_recent_rejects_reconsiderable, since they can be CPFP'd.
    119+    // we *do* want to request parents that are in RecentRejectsReconsiderableFilter, since they can be CPFP'd.
    120     if (p2p_inv && AlreadyHaveTx(gtxid, /*include_reconsiderable=*/true)) return true;
    121 
    122-    if (m_peer_info.count(peer) == 0) return false;
    123-    const auto& info = m_peer_info.at(peer).m_connection_info;
    124+    auto it = m_peer_info.find(peer);
    125+    if (it == m_peer_info.end()) return false;
    126+    const auto& info = it->second.m_connection_info;
    127     if (!info.m_relay_permissions && m_txrequest.Count(peer) >= MAX_PEER_TX_ANNOUNCEMENTS) {
    128         // Too many queued announcements for this peer
    129         return false;
    130@@ -259,20 +240,14 @@
    131     //
    132     // If we start tracking all announcers of orphans, we can restrict this logic to parent + child
    133     // pairs in which both were provided by the same peer, i.e. delete this step.
    134-    const auto cpfp_candidates_different_peer{m_orphanage.GetChildrenFromDifferentPeer(ptx, nodeid)};
    135-
    136+    auto cpfp_candidates_different_peer = m_orphanage.GetChildrenFromDifferentPeer(ptx, nodeid);
    137     // Find the first 1p1c that hasn't already been rejected. We randomize the order to not
    138     // create a bias that attackers can use to delay package acceptance.
    139     //
    140-    // Create a random permutation of the indices.
    141-    std::vector<size_t> tx_indices(cpfp_candidates_different_peer.size());
    142-    std::iota(tx_indices.begin(), tx_indices.end(), 0);
    143-    std::shuffle(tx_indices.begin(), tx_indices.end(), m_opts.m_rng);
    144-
    145-    for (const auto index : tx_indices) {
    146+    std::shuffle(cpfp_candidates_different_peer.begin(), cpfp_candidates_different_peer.end(), m_opts.m_rng);
    147+    for (const auto& [child_tx, child_sender] : cpfp_candidates_different_peer) {
    148         // If we already tried a package and failed for any reason, the combined hash was
    149-        // cached in m_lazy_recent_rejects_reconsiderable.
    150-        const auto [child_tx, child_sender] = cpfp_candidates_different_peer.at(index);
    151+        // cached in RecentRejectsReconsiderableFilter.
    152         Package maybe_cpfp_package{ptx, child_tx};
    153         if (!RecentRejectsReconsiderableFilter().contains(GetPackageHash(maybe_cpfp_package))) {
    154             return PackageToValidate{ptx, child_tx, nodeid, child_sender};
    155@@ -300,7 +275,7 @@
    156     // Whether we should call AddToCompactExtraTransactions at the end
    157     bool add_extra_compact_tx{first_time_failure};
    158     // Hashes to pass to AddKnownTx later
    159-    std::vector<uint256> unique_parents;
    160+    std::set<uint256> unique_parents;
    161     // Populated if failure is reconsiderable and eligible package is found.
    162     std::optional<node::PackageToValidate> package_to_validate;
    163 
    164@@ -312,16 +287,13 @@
    165 
    166             // Deduplicate parent txids, so that we don't have to loop over
    167             // the same parent txid more than once down below.
    168-            unique_parents.reserve(tx.vin.size());
    169             for (const CTxIn& txin : tx.vin) {
    170                 // We start with all parents, and then remove duplicates below.
    171-                unique_parents.push_back(txin.prevout.hash);
    172+                unique_parents.insert(txin.prevout.hash);
    173             }
    174-            std::sort(unique_parents.begin(), unique_parents.end());
    175-            unique_parents.erase(std::unique(unique_parents.begin(), unique_parents.end()), unique_parents.end());
    176 
    177-            // Distinguish between parents in m_lazy_recent_rejects and m_lazy_recent_rejects_reconsiderable.
    178-            // We can tolerate having up to 1 parent in m_lazy_recent_rejects_reconsiderable since we
    179+            // Distinguish between parents in m_lazy_recent_rejects and RecentRejectsReconsiderableFilter.
    180+            // We can tolerate having up to 1 parent in RecentRejectsReconsiderableFilter since we
    181             // submit 1p1c packages. However, fail immediately if any are in m_lazy_recent_rejects.
    182             std::optional<uint256> rejected_parent_reconsiderable;
    183             for (const uint256& parent_txid : unique_parents) {
    184@@ -330,7 +302,7 @@
    185                     break;
    186                 } else if (RecentRejectsReconsiderableFilter().contains(parent_txid) &&
    187                            !m_opts.m_mempool.exists(GenTxid::Txid(parent_txid))) {
    188-                    // More than 1 parent in m_lazy_recent_rejects_reconsiderable: 1p1c will not be
    189+                    // More than 1 parent in RecentRejectsReconsiderableFilter: 1p1c will not be
    190                     // sufficient to accept this package, so just give up here.
    191                     if (rejected_parent_reconsiderable.has_value()) {
    192                         fRejectedParents = true;
    193@@ -349,7 +321,7 @@
    194                     // Eventually we should replace this with an improved
    195                     // protocol for getting all unconfirmed parents.
    196                     const auto gtxid{GenTxid::Txid(parent_txid)};
    197-                    // Exclude m_lazy_recent_rejects_reconsiderable: the missing parent may have been
    198+                    // Exclude RecentRejectsReconsiderableFilter: the missing parent may have been
    199                     // previously rejected for being too low feerate. This orphan might CPFP it.
    200                     if (!AlreadyHaveTx(gtxid, /*include_reconsiderable=*/false)) {
    201                         AddTxAnnouncement(nodeid, gtxid, current_time, /*p2p_inv=*/false);
    202@@ -405,7 +377,7 @@
    203         // for concerns around weakening security of unupgraded nodes
    204         // if we start doing this too early.
    205         if (state.GetResult() == TxValidationResult::TX_RECONSIDERABLE) {
    206-            // If the result is TX_RECONSIDERABLE, add it to m_lazy_recent_rejects_reconsiderable
    207+            // If the result is TX_RECONSIDERABLE, add it to RecentRejectsReconsiderableFilter
    208             // because we should not download or submit this transaction by itself again, but may
    209             // submit it as part of a package later.
    210             RecentRejectsReconsiderableFilter().insert(ptx->GetWitnessHash().ToUint256());
    211@@ -493,18 +465,18 @@
    212         // due to node policy (vs. consensus). So we can't blanket penalize a
    213         // peer simply for relaying a tx that our m_lazy_recent_rejects has caught,
    214         // regardless of false positives.
    215-        return std::make_pair(false, std::nullopt);
    216+        return {false, std::nullopt};
    217     } else if (RecentRejectsReconsiderableFilter().contains(wtxid)) {
    218-        // When a transaction is already in m_lazy_recent_rejects_reconsiderable, we shouldn't submit
    219+        // When a transaction is already in RecentRejectsReconsiderableFilter, we shouldn't submit
    220         // it by itself again. However, look for a matching child in the orphanage, as it is
    221         // possible that they succeed as a package.
    222         LogDebug(BCLog::TXPACKAGES, "found tx %s (wtxid=%s) in reconsiderable rejects, looking for child in orphanage\n",
    223                  txid.ToString(), wtxid.ToString());
    224-        return std::make_pair(false, Find1P1CPackage(ptx, nodeid));
    225+        return {false, Find1P1CPackage(ptx, nodeid)};
    226     }
    227 
    228 
    229-    return std::make_pair(true, std::nullopt);
    230+    return {true, std::nullopt};
    231 }
    232 
    233 bool TxDownloadManagerImpl::HaveMoreWork(NodeId nodeid)
    234Index: src/node/txdownloadman.h
    235<+>UTF-8
    236===================================================================
    237diff --git a/src/node/txdownloadman.h b/src/node/txdownloadman.h
    238--- a/src/node/txdownloadman.h	(revision b543dc083ddbf43bb172b60ddb90609a600c86b5)
    239+++ b/src/node/txdownloadman.h	(date 1727088479738)
    240@@ -24,12 +24,12 @@
    241 
    242 /** Maximum number of in-flight transaction requests from a peer. It is not a hard limit, but the threshold at which
    243  *  point the OVERLOADED_PEER_TX_DELAY kicks in. */
    244-static constexpr int32_t MAX_PEER_TX_REQUEST_IN_FLIGHT = 100;
    245+static constexpr int32_t MAX_PEER_TX_REQUEST_IN_FLIGHT{100};
    246 /** Maximum number of transactions to consider for requesting, per peer. It provides a reasonable DoS limit to
    247  *  per-peer memory usage spent on announcements, while covering peers continuously sending INVs at the maximum
    248  *  rate (by our own policy, see INVENTORY_BROADCAST_PER_SECOND) for several minutes, while not receiving
    249  *  the actual transaction (from any peer) in response to requests for them. */
    250-static constexpr int32_t MAX_PEER_TX_ANNOUNCEMENTS = 5000;
    251+static constexpr int32_t MAX_PEER_TX_ANNOUNCEMENTS{5000};
    252 /** How long to delay requesting transactions via txids, if we have wtxid-relaying peers */
    253 static constexpr auto TXID_RELAY_DELAY{2s};
    254 /** How long to delay requesting transactions from non-preferred peers */
    255@@ -68,38 +68,28 @@
    256         m_senders{parent_sender, child_sender}
    257     {}
    258 
    259-    // Move ctor
    260-    PackageToValidate(PackageToValidate&& other) : m_txns{std::move(other.m_txns)}, m_senders{std::move(other.m_senders)} {}
    261-    // Copy ctor
    262-    PackageToValidate(const PackageToValidate& other) = default;
    263+    PackageToValidate(PackageToValidate&&) = default;
    264+    PackageToValidate(const PackageToValidate&) = default;
    265+    PackageToValidate& operator=(PackageToValidate&&) = default;
    266 
    267-    // Move assignment
    268-    PackageToValidate& operator=(PackageToValidate&& other) {
    269-        this->m_txns = std::move(other.m_txns);
    270-        this->m_senders = std::move(other.m_senders);
    271-        return *this;
    272-    }
    273-
    274-    std::string ToString() const {
    275+    std::string ToString() const
    276+    {
    277         Assume(m_txns.size() == 2);
    278-        return strprintf("parent %s (wtxid=%s, sender=%d) + child %s (wtxid=%s, sender=%d)",
    279-                         m_txns.front()->GetHash().ToString(),
    280-                         m_txns.front()->GetWitnessHash().ToString(),
    281-                         m_senders.front(),
    282-                         m_txns.back()->GetHash().ToString(),
    283-                         m_txns.back()->GetWitnessHash().ToString(),
    284-                         m_senders.back());
    285+        auto format_tx{[&](const CTransactionRef& tx, NodeId sender) {
    286+            return strprintf("%s (wtxid=%s, sender=%d)", tx->GetHash().ToString(), tx->GetWitnessHash().ToString(), sender);
    287+        }};
    288+        return strprintf("parent %s + child %s", format_tx(m_txns.front(), m_senders.front()), format_tx(m_txns.back(), m_senders.back()));
    289     }
    290 };
    291-struct RejectedTxTodo
    292-{
    293+struct RejectedTxTodo {
    294     bool m_should_add_extra_compact_tx;
    295-    std::vector<uint256> m_unique_parents;
    296+    std::set<uint256> m_unique_parents;
    297     std::optional<PackageToValidate> m_package_to_validate;
    298 };
    299 
    300 
    301-class TxDownloadManager {
    302+class TxDownloadManager
    303+{
    304     const std::unique_ptr<TxDownloadManagerImpl> m_impl;
    305 
    306 public:
    307@@ -118,7 +108,7 @@
    308     void DisconnectedPeer(NodeId nodeid);
    309 
    310     /** New inv has been received. May be added as a candidate to txrequest.
    311-     * [@param](/bitcoin-bitcoin/contributor/param/)[in] p2p_inv     When true, only add this announcement if we don't already have the tx.
    312+     * [@param](/bitcoin-bitcoin/contributor/param/)[in] p2p_inv When true, only add this announcement if we don't already have the tx.
    313      * Returns true if this was a dropped inv (p2p_inv=true and we already have the tx), false otherwise. */
    314     bool AddTxAnnouncement(NodeId peer, const GenTxid& gtxid, std::chrono::microseconds now, bool p2p_inv);
    315 
    316@@ -153,7 +143,6 @@
    317 
    318     /** Check that all data structures that track per-peer information have nothing for this peer. */
    319     void CheckIsEmpty(NodeId nodeid) const;
    320-
    321 };
    322 } // namespace node
    323 #endif // BITCOIN_NODE_TXDOWNLOADMAN_H
    324Index: src/test/fuzz/txdownloadman_one_honest_peer.cpp
    325<+>UTF-8
    326===================================================================
    327diff --git a/src/test/fuzz/txdownloadman_one_honest_peer.cpp b/src/test/fuzz/txdownloadman_one_honest_peer.cpp
    328--- a/src/test/fuzz/txdownloadman_one_honest_peer.cpp	(revision b543dc083ddbf43bb172b60ddb90609a600c86b5)
    329+++ b/src/test/fuzz/txdownloadman_one_honest_peer.cpp	(date 1727088723398)
    330@@ -16,9 +16,9 @@
    331 #include <test/util/script.h>
    332 #include <test/util/setup_common.h>
    333 #include <test/util/txmempool.h>
    334+#include <txmempool.h>
    335 #include <util/hasher.h>
    336 #include <util/rbf.h>
    337-#include <txmempool.h>
    338 #include <validation.h>
    339 #include <validationinterface.h>
    340 
    341@@ -81,7 +81,8 @@
    342     PossibleResponse::DEFER,
    343 };
    344 
    345-class TestPeer {
    346+class TestPeer
    347+{
    348 public:
    349     // Queue of getdatas to respond to: process from front, add to back.
    350     std::deque<GenTxid> m_getdata_queue;
    351@@ -118,7 +119,8 @@
    352     // to announce the target transaction. Updates m_sent_inv if this happens.
    353     // When force_announce=true, if this is a "good" peer and the transaction hasn't already been
    354     // announced, always return INV_REAL.
    355-    PossibleMessage SendOneMessage(FuzzedDataProvider& fuzzed_data_provider, bool force_announce = false) {
    356+    PossibleMessage SendOneMessage(FuzzedDataProvider& fuzzed_data_provider, bool force_announce = false)
    357+    {
    358         if (m_good) {
    359             // "Good" peers only send 1 unsolicited message: announcement of the tx.
    360             if (!m_sent_inv) {
    361@@ -138,24 +140,23 @@
    362 
    363     // Process up to one message from the front of m_getdata_queue. The size of m_getdata_queue may
    364     // not change, as DEFER may just push it to the back again.
    365-    std::pair<PossibleResponse, std::optional<GenTxid>> ProcessOneMessage(FuzzedDataProvider& fuzzed_data_provider) {
    366+    std::pair<PossibleResponse, std::optional<GenTxid>> ProcessOneMessage(FuzzedDataProvider& fuzzed_data_provider)
    367+    {
    368         // Nothing to do if no messages to process
    369         if (m_getdata_queue.empty()) return {PossibleResponse::NOTHING, std::nullopt};
    370 
    371-        auto request = m_getdata_queue.front();
    372-        auto response{PossibleResponse::TX_REAL};
    373-        if (!m_good) response = fuzzed_data_provider.PickValueInArray(NON_GOOD_RESPONSES);
    374-
    375-        // If DEFER, pop the request to the back of the queue and return nothing.
    376-        // This simulates responding out of order and/or stalling.
    377-        if (response == PossibleResponse::DEFER) {
    378-            m_getdata_queue.push_back(request);
    379-            m_getdata_queue.pop_front();
    380+        auto request{m_getdata_queue.front()};
    381+        m_getdata_queue.pop_front();
    382+
    383+        auto response{m_good ? PossibleResponse::TX_REAL : fuzzed_data_provider.PickValueInArray(NON_GOOD_RESPONSES)};
    384+        if (response == PossibleResponse::DEFER) {
    385+            // Move the request from the front to the back of the queue and return nothing.
    386+            // This simulates responding out of order and/or stalling.
    387+            m_getdata_queue.emplace_back(std::move(request));
    388             return {PossibleResponse::NOTHING, std::nullopt};
    389-        }
    390-
    391-        m_getdata_queue.pop_front();
    392-        return {response, request};
    393+        } else {
    394+            return {response, request};
    395+        }
    396     }
    397 };
    398 
    399@@ -236,43 +237,39 @@
    400         const auto& malleated_wtxid = malleated_tx->GetWitnessHash().ToUint256();
    401 
    402         // The first tx in the pair needs to match by wtxid.
    403-        TRANSACTIONS.emplace(target_wtxid, std::make_pair(correct_tx, malleated_tx));
    404-        TRANSACTIONS.emplace(target_txid, std::make_pair(correct_tx, correct_tx));
    405+        TRANSACTIONS.try_emplace(target_wtxid, correct_tx, malleated_tx);
    406+        TRANSACTIONS.try_emplace(target_txid, correct_tx, correct_tx);
    407 
    408-        TRANSACTIONS.emplace(malleated_wtxid, std::make_pair(malleated_tx, stripped_tx));
    409+        TRANSACTIONS.try_emplace(malleated_wtxid, malleated_tx, stripped_tx);
    410     }
    411 }
    412 
    413 static CTransactionRef GetCorrectTx(const std::map<uint256, std::pair<CTransactionRef, CTransactionRef>> txmap, const GenTxid& gtxid)
    414 {
    415-    auto tx_it = txmap.find(gtxid.GetHash());
    416-    if (tx_it != txmap.end()) {
    417+    if (auto tx_it = txmap.find(gtxid.GetHash()); tx_it != txmap.end()) {
    418         return tx_it->second.first;
    419     }
    420     return nullptr;
    421 }
    422 static CTransactionRef GetStrippedTx(const std::map<uint256, std::pair<CTransactionRef, CTransactionRef>> txmap, const GenTxid& gtxid)
    423 {
    424-    auto tx_it = txmap.find(gtxid.GetHash());
    425-    if (tx_it != txmap.end()) {
    426+    if (auto tx_it = txmap.find(gtxid.GetHash()); tx_it != txmap.end()) {
    427         return StripWitness(tx_it->second.first);
    428     }
    429     return nullptr;