Skip to content

Conversation

furszy
Copy link
Member

@furszy furszy commented Jun 11, 2025

Context:
Every time a block is connected, a BlockConnected() event is appended to the validation interface queue. This queue is consumed sequentially by a single worker thread. To avoid excessive memory usage, the queue is limited to 10 events at a time, and we stop processing new blocks until the queued events are handled.

Issue:
Within the PeerManager::BlockConnected() listener, we traverse the block twice inside the transaction download manager — despite not needing to handle orphans or transaction requests during IBD.

Extra info about the changes:
The new arg added to BlockConnected() in the first commit is primarily meant to avoid locking cs_main on the
listener side (to avoid calling IsInitialBlockDownload() there).
Another way of implementing this could be to add a bool field enabled to the TxDownloadManager class that is only
set to true when we are out of IBD, so that we don't have to include a new arg inside the block connection event.
This is open for discussion. Could try implementing this second approach too.

@DrahtBot
Copy link
Contributor

DrahtBot commented Jun 11, 2025

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

Code Coverage & Benchmarks

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

Reviews

See the guideline for information on the review process.
A summary of reviews will appear here.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #32941 (p2p: TxOrphanage revamp cleanups by glozow)
  • #32827 (mempool: Avoid needless vtx iteration during IBD by l0rinc)
  • #32631 (refactor: Convert GenTxid to std::variant by marcofleon)
  • #32189 (refactor: Txid type safety (parent PR) by marcofleon)
  • #31829 (p2p: improve TxOrphanage denial of service bounds by glozow)
  • #30214 (refactor: Improve assumeutxo state representation by ryanofsky)
  • #30116 (p2p: Fill reconciliation sets (Erlay) attempt 2 by sr-gi)
  • #29415 (Broadcast own transactions only via short-lived Tor or I2P connections by vasild)

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.

@DrahtBot DrahtBot added the P2P label Jun 11, 2025
Copy link
Contributor

@l0rinc l0rinc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only had time for adding a few questions and quickly checking the expected effect this will have on IBD.

A flame graph I did this week for master, showing a -reindex until 900k blocks shows this part of the code to be 0.07% of IBD (~20 seconds). It can still make sense to do it, we should avoid doing unnecessary work, just setting the expectations straight.

image

Left a few notes, the guard makes sense to me, but the part I passionately disagree with is the boolean argument train - is there a simpler way to avoid that either via a different guard or the locally available m_initial_sync_finished (we've been locking before anyway).

@@ -246,6 +246,9 @@ bool TxOrphanage::HaveTxToReconsider(NodeId peer)

void TxOrphanage::EraseForBlock(const CBlock& block)
{
// No orphans, nothing to do
if (m_orphans.empty()) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense on its own I think - it's similar to another draft PR I had (bitcoin-dev-tools#167). If you think it's related and adds anything, feel free to cherry-pick it here.

Copy link
Member Author

@furszy furszy Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense on its own I think - it's similar to another draft PR I had (bitcoin-dev-tools#167). If you think it's related and adds anything, feel free to cherry-pick it here.

That's cool, will check it in the coming days.
First quick glance it seems that if the mempool is empty; we could avoid calling removeForBlock entirely. Which will save us from adding another callback into the validation queue too, which will leave space for processing another block during ibd.
Yet, that was a fast check. Should check who is using the MempoolTransactionsRemovedForBlock event.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To simplify review, could you split it into several commits - one changing the m_initial_sync(_finished) implementation, one adding it next to role == ChainstateRole::BACKGROUND and one with this change - explaining the context (which are a bit muffled for me currently) in each commit message?

I'm also curious if it's possible to get back to being in IBD after we're finished (e.g. after being offline for weeks, i.e. if (chain.Tip()->Time() < Now<NodeSeconds>() - m_options.max_tip_age) {).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened it separately in #32827, please resolve this comment

@@ -246,6 +246,9 @@ bool TxOrphanage::HaveTxToReconsider(NodeId peer)

void TxOrphanage::EraseForBlock(const CBlock& block)
{
// No orphans, nothing to do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the comment redundant, it states exactly what the code already states clearly, doesn't add any new info

@@ -1994,7 +1994,7 @@ void PeerManagerImpl::BlockConnected(

// The following task can be skipped since we don't maintain a mempool for
// the ibd/background chainstate.
if (role == ChainstateRole::BACKGROUND) {
if (is_ibd || role == ChainstateRole::BACKGROUND) {
return;
}
LOCK(m_tx_download_mutex);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can safely skip this because we're never requesting single transactions during IBD (i.e. AlreadyHaveTx isn't needed during IBD)?

for (const auto& ptx : pblock->vtx) {
    RecentConfirmedTransactionsFilter().insert(ptx->GetHash().ToUint256());
    if (ptx->HasWitness()) {
        RecentConfirmedTransactionsFilter().insert(ptx->GetWitnessHash().ToUint256());
    }
    m_txrequest.ForgetTxHash(ptx->GetHash());
    m_txrequest.ForgetTxHash(ptx->GetWitnessHash());
}

Can we guard this by some other criteria instead of the IBD boolean, like we're doing with EraseForBlock, e.g.

if (!m_txrequest.Size()) return;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can safely skip this because we're never requesting single transactions during IBD (i.e. AlreadyHaveTx isn't needed during IBD)?

for (const auto& ptx : pblock->vtx) {
    RecentConfirmedTransactionsFilter().insert(ptx->GetHash().ToUint256());
    if (ptx->HasWitness()) {
        RecentConfirmedTransactionsFilter().insert(ptx->GetWitnessHash().ToUint256());
    }
    m_txrequest.ForgetTxHash(ptx->GetHash());
    m_txrequest.ForgetTxHash(ptx->GetWitnessHash());
}

Can we guard this by some other criteria instead of the IBD boolean, like we're doing with EraseForBlock, e.g.

if (!m_txrequest.Size()) return;

I thought about it but didn't do it on purpose. The reasoning is that we still need to add the txs into the recently confirmed filters post-IBD even when we don't have any tx requests. In other words, we still need to traverse the block. Could at most skip the last two lines with the emptiness check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could at most skip the last two lines with the emptiness check

Not sure that helps

Comment on lines 1995 to 1997
// The following task can be skipped since we don't maintain a mempool for
// the ibd/background chainstate.
if (role == ChainstateRole::BACKGROUND) {
if (is_ibd || role == ChainstateRole::BACKGROUND) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the comment (already mentioning IBD) still accurate?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the comment (already mentioning IBD) still accurate?

The comment was accurate, but the code didn’t behave as described. Now it does.

@@ -1975,7 +1975,8 @@ void PeerManagerImpl::ActiveTipChange(const CBlockIndex& new_tip, bool is_ibd)
void PeerManagerImpl::BlockConnected(
ChainstateRole role,
const std::shared_ptr<const CBlock>& pblock,
const CBlockIndex* pindex)
const CBlockIndex* pindex,
bool is_ibd)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there should be a better way to do this than passing around a boolean over so many layers. I'll review it properly this week.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there should be a better way to do this than passing around a boolean over so many layers. I'll review it properly this week.

Did you check the second approach proposed in the PR description?

@@ -246,6 +246,9 @@ bool TxOrphanage::HaveTxToReconsider(NodeId peer)

void TxOrphanage::EraseForBlock(const CBlock& block)
{
// No orphans, nothing to do
if (m_orphans.empty()) return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a common-sense change by itself 👍

Copy link
Member Author

@furszy furszy Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tiny note: this also skip post-IBD blocks when we don't have any orphans.
--Credits to @mzumsande for pointing it out too--
We could do the same for the tx requests tracker loop but.. that wouldn't change much as we still need to traverse the block for the "recently confirmed" filter anyway.

@@ -1993,7 +1994,7 @@ void PeerManagerImpl::BlockConnected(

// The following task can be skipped since we don't maintain a mempool for
// the ibd/background chainstate.
if (role == ChainstateRole::BACKGROUND) {
if (is_ibd || role == ChainstateRole::BACKGROUND) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't checking role == ChainstateRole::BACKGROUND || m_chainman.IsInitialBlockDownload() be good enough? I realize it is racy, the condition may already be false even when the last few ibd blocks are still being processed, but I don't think that really matters here. We also use it similarly in the BlockChecked method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's why I asked for !m_initial_sync_finished as well.
If the race is biased towards reenabling it too early (rather than later), I agree that it should be fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

furszy reminded me that BlockChecked is run synchronously, so I don't think that comparison makes sense after all. I guess this comment can be resolved.

Copy link
Member Author

@furszy furszy Jun 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't checking role == ChainstateRole::BACKGROUND || m_chainman.IsInitialBlockDownload() be good enough? I realize it is racy, the condition may already be false even when the last few ibd blocks are still being processed, but I don't think that really matters here. We also use it similarly in the BlockChecked method.

The diff between BlockConnected() and BlockChecked() is that the former one runs async while the latter runs synchronously. So the idea was to avoid locking cs_main in a different thread which delays other parts of the node.

I also thought about using the 'm_initial_sync_finished' bool for this, but that's not good because the field is calculated at a different time interval in the stale tip check thread (we could compute it more often but that's a different story).

The last idea that had was the enabled flag inside the TxDownloadManager (check the PR description, you might like this one more).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last idea that had was the enabled flag inside the TxDownloadManager (check the PR description, you might like this one more).

I saw that but not yet sure until I see it in action. I definitely dislike the current arg passing, so I'd vote for that if we really need this param

Copy link
Member Author

@furszy furszy Jun 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw that but not yet sure until I see it in action. I definitely dislike the current arg passing, so I'd vote for that if we really need this param

Ok, I'm a ~0 on this. I think this field is special enough (reason below) to be signaled as a separate arg or it could be wrapped into a struct — similar to how it's done in the interfaces::Chain::Notifications class. With this, I'm not saying to not try the TxDownloadManager enabled flag approach, I just think that the current approach is slightly more useful for future listeners usages.

The only thing I'm somewhat convinced of is that, just like listeners are informed about blocks being downloaded through AssumeUTXO background chain validation, it would be nice if they are aware of IBD blocks — otherwise, sooner or later they will end up re-implementing the same racy/locking isIBD() logic across multiple layers (which could be fine for some cases but it doesn't seem to be generally good).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be nice if they are aware of IBD blocks — otherwise [...] they will end up re-implementing

Is there a more reusable way we could implement this?

@TheCharlatan
Copy link
Contributor

One thing that still irks me a bit is that we are communicating more ChainstateManager state through a Chainstate callback. I think ideally the chainstate should not have to know if it is in ibd or not, but that is a difficult thing to achieve with out current code. What does seem non-ideal is that re-indexing is treated the same is_ibd. Maybe the SynchronizationState from the headerTip notification would give us more flexibility in that regard if needed in the future?

Context:
Every time a block is connected, a 'BlockConnected' event is appended to the validation
interface queue. This queue is consumed sequentially by a single worker thread.
To avoid excessive memory usage, the queue is limited to 10 events at a time, and
we stop processing new blocks until the queued events are handled.

Issue:
Within the 'PeerManager::BlockConnected()' listener, we traverse the block twice
inside the transaction download manager — despite not needing to handle orphans or
transaction requests during IBD.
This redundant work adds unnecessary delay to the initial block download process.
@furszy furszy force-pushed the 2025_net_avoid_traversing_block_twice branch from e48bfeb to 8b9d396 Compare June 17, 2025 18:42
@furszy
Copy link
Member Author

furszy commented Jun 17, 2025

One thing that still irks me a bit is that we are communicating more ChainstateManager state through a Chainstate callback. I think ideally the chainstate should not have to know if it is in ibd or not, but that is a difficult thing to achieve with out current code.

Hmm yeah, we would need to decouple the block download logic from the chain state class first. This reminds me to #27837, which is a small step in that direction.

What does seem non-ideal is that re-indexing is treated the same is_ibd. Maybe the SynchronizationState from the headerTip notification would give us more flexibility in that regard if needed in the future?

Check it now. Ended up reimplementing it differently.

Copy link
Member

@maflcko maflcko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to first add proper tests for the touched code in another pull, before refactoring the code for a 0.07% speedup. (The refactor broke the code, as I see it)

Comment on lines 5269 to -5270
if (!m_initial_sync_finished && CanDirectFetch()) {
m_connman.StartExtraBlockRelayPeers();
m_initial_sync_finished = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m_initial_sync_finished will normally be set to true, so this is dead code now. Surprising that no tests failed ...

Copy link
Contributor

@l0rinc l0rinc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this new patch a lot more, setting IBD is a lot cleaner this way.
Somebody with more experience should validate whether that's correct or not.
My main suggestion is splitting to commits where the messages put the 3 distinct changes in context - and maybe to invert the naming of a the new field which is always negated at use site anyway.

Patch
diff --git a/src/init.cpp b/src/init.cpp
index 88fe531119..80b2fc7d6a 100644
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -1909,7 +1909,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
     // ********************************************************* Step 12: start node
 
     int64_t best_block_time{};
-    bool initial_sync_finished{false};
+    bool initial_sync{true};
     {
         LOCK(chainman.GetMutex());
         const auto& tip{*Assert(chainman.ActiveTip())};
@@ -1925,10 +1925,10 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
             tip_info->header_height = chainman.m_best_header->nHeight;
             tip_info->header_time = chainman.m_best_header->GetBlockTime();
         }
-        initial_sync_finished = !chainman.IsInitialBlockDownload();
+        initial_sync = chainman.IsInitialBlockDownload();
     }
     LogPrintf("nBestHeight = %d\n", chain_active_height);
-    if (node.peerman) node.peerman->SetBestBlock(chain_active_height, std::chrono::seconds{best_block_time}, initial_sync_finished);
+    if (node.peerman) node.peerman->SetBestBlock(chain_active_height, best_block_time, initial_sync);
 
     // Map ports with NAT-PMP
     StartMapPort(args.GetBoolArg("-natpmp", DEFAULT_NATPMP));
diff --git a/src/net_processing.cpp b/src/net_processing.cpp
index 97e3e05d4f..89229d6cae 100644
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -511,7 +511,7 @@ public:
         EXCLUSIVE_LOCKS_REQUIRED(!m_tx_download_mutex);
     void BlockDisconnected(const std::shared_ptr<const CBlock> &block, const CBlockIndex* pindex) override
         EXCLUSIVE_LOCKS_REQUIRED(!m_tx_download_mutex);
-    void UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload) override
+    void UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool initial_sync) override
         EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
     void BlockChecked(const CBlock& block, const BlockValidationState& state) override
         EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
@@ -537,11 +537,11 @@ public:
     PeerManagerInfo GetInfo() const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
     void SendPings() override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
     void RelayTransaction(const uint256& txid, const uint256& wtxid) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
-    void SetBestBlock(int height, std::chrono::seconds time, bool initial_sync_finished) override
+    void SetBestBlock(int height, uint64_t block_time, bool initial_sync) override
     {
         m_best_height = height;
-        m_best_block_time = time;
-        m_initial_sync_finished = initial_sync_finished;
+        m_best_block_time = std::chrono::seconds{block_time};
+        m_initial_sync = initial_sync;
     };
     void UnitTestMisbehaving(NodeId peer_id) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex) { Misbehaving(*Assert(GetPeerRef(peer_id)), ""); };
     void ProcessMessage(CNode& pfrom, const std::string& msg_type, DataStream& vRecv,
@@ -789,9 +789,9 @@ private:
 
     bool RejectIncomingTxs(const CNode& peer) const;
 
-    /** Whether we've completed initial sync yet, for determining when to turn
+    /** Whether we're still in initial sync, for determining when to turn
       * on extra block-relay-only peers. */
-    std::atomic<bool> m_initial_sync_finished{false};
+    std::atomic<bool> m_initial_sync{true};
 
     /** Protects m_peer_map. This mutex must not be locked while holding a lock
      *  on any of the mutexes inside a Peer object. */
@@ -1994,7 +1994,7 @@ void PeerManagerImpl::BlockConnected(
 
     // The following task can be skipped since we don't maintain a mempool for
     // the ibd/background chainstate.
-    if (!m_initial_sync_finished || role == ChainstateRole::BACKGROUND) {
+    if (m_initial_sync || role == ChainstateRole::BACKGROUND) {
         return;
     }
     LOCK(m_tx_download_mutex);
@@ -2066,12 +2066,12 @@ void PeerManagerImpl::NewPoWValidBlock(const CBlockIndex *pindex, const std::sha
  * Update our best height and announce any block hashes which weren't previously
  * in m_chainman.ActiveChain() to our peers.
  */
-void PeerManagerImpl::UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload)
+void PeerManagerImpl::UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool initial_sync)
 {
-    SetBestBlock(pindexNew->nHeight, std::chrono::seconds{pindexNew->GetBlockTime()}, !fInitialDownload);
+    SetBestBlock(pindexNew->nHeight, pindexNew->GetBlockTime(), initial_sync);
 
     // Don't relay inventory during initial block download.
-    if (fInitialDownload) return;
+    if (initial_sync) return;
 
     // Find the hashes of all blocks that weren't previously in the best chain.
     std::vector<uint256> vHashes;
@@ -5266,8 +5266,9 @@ void PeerManagerImpl::CheckForStaleTipAndEvictPeers()
         m_stale_tip_check_time = now + STALE_CHECK_INTERVAL;
     }
 
-    if (!m_initial_sync_finished && CanDirectFetch()) {
+    if (m_initial_sync && CanDirectFetch()) {
         m_connman.StartExtraBlockRelayPeers();
+        // TODO? m_initial_sync = false;
     }
 }
 
diff --git a/src/net_processing.h b/src/net_processing.h
index 5a5980a9da..d7ea6a12a3 100644
--- a/src/net_processing.h
+++ b/src/net_processing.h
@@ -124,8 +124,8 @@ public:
     /** Send ping message to all peers */
     virtual void SendPings() = 0;
 
-    /** Set the height of the best block and its time (seconds since epoch), and indicate whether historical block sync has completed. */
-    virtual void SetBestBlock(int height, std::chrono::seconds time, bool initial_sync_finished) = 0;
+    /** Set the height of the best block, its block time, and whether block sync has completed. */
+    virtual void SetBestBlock(int height, uint64_t block_time, bool initial_sync) = 0;
 
     /* Public for unit testing. */
     virtual void UnitTestMisbehaving(NodeId peer_id) = 0;
diff --git a/src/test/peerman_tests.cpp b/src/test/peerman_tests.cpp
index 30d680ffde..0bedae7859 100644
--- a/src/test/peerman_tests.cpp
+++ b/src/test/peerman_tests.cpp
@@ -41,8 +41,7 @@ BOOST_AUTO_TEST_CASE(connections_desirable_service_flags)
     // Make peerman aware of the initial best block and verify we accept limited peers when we start close to the tip time.
     auto tip = WITH_LOCK(::cs_main, return m_node.chainman->ActiveChain().Tip());
     uint64_t tip_block_time = tip->GetBlockTime();
-    int tip_block_height = tip->nHeight;
-    peerman->SetBestBlock(tip_block_height, std::chrono::seconds{tip_block_time}, /*initial_sync_finished=*/true);
+    peerman->SetBestBlock(tip->nHeight, tip_block_time, /*initial_sync=*/false);
 
     SetMockTime(tip_block_time + 1); // Set node time to tip time
     BOOST_CHECK(peerman->GetDesirableServiceFlags(peer_flags) == ServiceFlags(NODE_NETWORK_LIMITED | NODE_WITNESS));
diff --git a/src/txorphanage.cpp b/src/txorphanage.cpp
index ca47bf949f..80d25df047 100644
--- a/src/txorphanage.cpp
+++ b/src/txorphanage.cpp
@@ -246,7 +246,6 @@ bool TxOrphanage::HaveTxToReconsider(NodeId peer)
 
 void TxOrphanage::EraseForBlock(const CBlock& block)
 {
-    // No orphans, nothing to do
     if (m_orphans.empty()) return;
 
     std::vector<Wtxid> vOrphanErase;

@@ -790,7 +791,7 @@ class PeerManagerImpl final : public PeerManager

/** Whether we've completed initial sync yet, for determining when to turn
* on extra block-relay-only peers. */
bool m_initial_sync_finished GUARDED_BY(cs_main){false};
std::atomic<bool> m_initial_sync_finished{false};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for using an atomic here!

Note: the usages are always inverted now, can we rather have std::atomic<bool> m_initial_sync{true} instead?

@@ -246,6 +246,9 @@ bool TxOrphanage::HaveTxToReconsider(NodeId peer)

void TxOrphanage::EraseForBlock(const CBlock& block)
{
// No orphans, nothing to do
if (m_orphans.empty()) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To simplify review, could you split it into several commits - one changing the m_initial_sync(_finished) implementation, one adding it next to role == ChainstateRole::BACKGROUND and one with this change - explaining the context (which are a bit muffled for me currently) in each commit message?

I'm also curious if it's possible to get back to being in IBD after we're finished (e.g. after being offline for weeks, i.e. if (chain.Tip()->Time() < Now<NodeSeconds>() - m_options.max_tip_age) {).

@@ -5267,7 +5268,6 @@ void PeerManagerImpl::CheckForStaleTipAndEvictPeers()

if (!m_initial_sync_finished && CanDirectFetch()) {
m_connman.StartExtraBlockRelayPeers();
m_initial_sync_finished = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this introduce any behavioral change? (i.e. m_initial_sync_finished being set to true at a different lifecycle)

}
LogPrintf("nBestHeight = %d\n", chain_active_height);
if (node.peerman) node.peerman->SetBestBlock(chain_active_height, std::chrono::seconds{best_block_time});
if (node.peerman) node.peerman->SetBestBlock(chain_active_height, std::chrono::seconds{best_block_time}, initial_sync_finished);
Copy link
Contributor

@l0rinc l0rinc Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: is the guard only meant for tests or can this happen in prod as well? Given that we only need initial_sync_finished when this is set, was wondering if it would make sense to guard the initial_sync_finished = !chainman.IsInitialBlockDownload() as well - but not sure if that makes it clearer or more cumbersome. Just documenting my questions, no action needed.

@@ -124,8 +124,8 @@ class PeerManager : public CValidationInterface, public NetEventsInterface
/** Send ping message to all peers */
virtual void SendPings() = 0;

/** Set the height of the best block and its time (seconds since epoch). */
virtual void SetBestBlock(int height, std::chrono::seconds time) = 0;
/** Set the height of the best block and its time (seconds since epoch), and indicate whether historical block sync has completed. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • indicate doesn't hint that we're actually setting it as well
  • historical might not be accurate given that we can re-enter IBD mode I think

@@ -1994,7 +1994,7 @@ void PeerManagerImpl::BlockConnected(

// The following task can be skipped since we don't maintain a mempool for
// the ibd/background chainstate.
if (role == ChainstateRole::BACKGROUND) {
if (is_ibd || role == ChainstateRole::BACKGROUND) {
return;
}
LOCK(m_tx_download_mutex);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could at most skip the last two lines with the emptiness check

Not sure that helps

@@ -537,10 +537,11 @@ class PeerManagerImpl final : public PeerManager
PeerManagerInfo GetInfo() const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
void SendPings() override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
void RelayTransaction(const uint256& txid, const uint256& wtxid) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
void SetBestBlock(int height, std::chrono::seconds time) override
void SetBestBlock(int height, std::chrono::seconds time, bool initial_sync_finished) override
Copy link
Contributor

@l0rinc l0rinc Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that we're always calling this with tip->GetBlockTime() and converting it to seconds on the call sites, we might simplify it to accept uint64_t block_time and convert to seconds inside instead. Not sure it's better, just noticed a pattern...

@furszy
Copy link
Member Author

furszy commented Jun 18, 2025

m_initial_sync_finished will normally be set to true, so this is dead code now. Surprising that no tests failed ...

yeah.. This reminded me why I didn’t use the m_initial_sync_finished flag in the previous version.. it is being set differently because it can toggle back and forth, while the IBD one doesn’t. We had a whole discussion about this in #28170.
Before getting into any detail, will think more about the approach (not this week) and whether it’s worth embarking on a larger journey just for this change.

@DrahtBot
Copy link
Contributor

🐙 This pull request conflicts with the target branch and needs rebase.

glozow added a commit that referenced this pull request Jul 21, 2025
249889b orphanage: avoid vtx iteration when no orphans (furszy)
41ad2be mempool: Avoid expensive loop in `removeForBlock` during IBD (Lőrinc)

Pull request description:

  During Initial Block Download, the mempool is usually empty, but `CTxMemPool::removeForBlock` is still called for every connected block where we:
  * iterate over every transaction in the block even though none will be found in the empty `mapTx`, always leaving `txs_removed_for_block` empty...
  * which is pre-allocated regardless with `40 bytes * vtx.size()`, even though it will always remain empty.

  Similarly to #32730 (comment), this change introduces a minor performance & memory optimization by only executing the loop if any of the affected mempool maps have any contents. The second commit is cherry-picked from there since it's related to this change as well.

ACKs for top commit:
  optout21:
    ACK 249889b
  glozow:
    ACK 249889b
  ismaelsadeeq:
    reACK 249889b

Tree-SHA512: 80d06ff1515164529cdc3ad21db3041bb5b2a1d4b72ba9e6884cdf40c5f1477fee7479944b8bca32a6f0bf27c4e5501fccd085f6041a2dbb101438629cfb9e4b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants