validation: Explicitly move blocks to validation signals #34704

pull sedited wants to merge 3 commits into bitcoin:master from sedited:block_connected_move changing 3 files +54 −42
  1. sedited commented at 9:07 am on March 1, 2026: contributor

    This enforces behaviour that is currently already implicit: The destructor for blocks runs mostly in the scheduler thread. The change should make it a bit clearer what the ownership semantics for these validation signals are.

    BlockConnected already takes a reference to a block that is emplaced in connected_blocks. Once connected_blocks is iterated through, it is not reused. Similarly BlockDisconnected currently takes a reference to a block that is discarded after the call to it. Note that this does not give the guarantee that blocks’ lifetimes are extended by other means once they are connected. For example after IBD, the block’s lifetime is extended in net_processing’s m_most_recent_block and ActivateBestChain itself takes a copy of the block’s shared pointer, meaning its caller may delay de-allocation.

  2. DrahtBot added the label Validation on Mar 1, 2026
  3. DrahtBot commented at 9:07 am on March 1, 2026: contributor

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

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK maflcko, stickies-v, frankomosh
    Stale ACK arejula27, rkrux

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

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #34803 (mempool: asynchronous mempool fee rate diagram updates via validation interface by ismaelsadeeq)
    • #34075 (fees: Introduce Mempool Based Fee Estimation to reduce overestimation by ismaelsadeeq)

    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. in src/validationinterface.cpp:165 in 76d1bc9bd3
    162 #define ENQUEUE_AND_LOG_EVENT(event, fmt, name, ...)           \
    163     do {                                                       \
    164         auto local_name = (name);                              \
    165         LOG_EVENT("Enqueuing " fmt, local_name, __VA_ARGS__);  \
    166-        m_internals->m_task_runner->insert([=] { \
    167+        m_internals->m_task_runner->insert([=, event = std::move(event)] { \
    


    stickies-v commented at 5:47 am on March 2, 2026:

    I think the ENQUEUE_AND_LOG_EVENT change could use its own commit, since this macro is used by many more functions than just BlockConnected

    I’m also a bit concerned about a macro opaquely making event an rvalue. It’s more verbose, but I think letting the caller move it would be safer?

    (the static_assert is optional)

     0diff --git a/src/validationinterface.cpp b/src/validationinterface.cpp
     1index 85827115a3..1cbbd43d35 100644
     2--- a/src/validationinterface.cpp
     3+++ b/src/validationinterface.cpp
     4@@ -157,15 +157,17 @@ void ValidationSignals::SyncWithValidationInterfaceQueue()
     5 // Use a macro instead of a function for conditional logging to prevent
     6 // evaluating arguments when logging is not enabled.
     7 //
     8-// NOTE: The lambda captures the event by move and all other variables by value.
     9-#define ENQUEUE_AND_LOG_EVENT(event, fmt, name, ...)           \
    10-    do {                                                       \
    11-        auto local_name = (name);                              \
    12-        LOG_EVENT("Enqueuing " fmt, local_name, __VA_ARGS__);  \
    13-        m_internals->m_task_runner->insert([=, event = std::move(event)] { \
    14-            LOG_EVENT(fmt, local_name, __VA_ARGS__);           \
    15-            event();                                           \
    16-        });                                                    \
    17+// NOTE: The lambda captures all local variables by value.
    18+#define ENQUEUE_AND_LOG_EVENT(event, fmt, name, ...)                    \
    19+    do {                                                                \
    20+        static_assert(std::is_rvalue_reference_v<decltype((event))>,    \
    21+                      "event must be passed as an rvalue");             \
    22+        auto local_name = (name);                                       \
    23+        LOG_EVENT("Enqueuing " fmt, local_name, __VA_ARGS__);           \
    24+        m_internals->m_task_runner->insert([=, local_event = (event)] { \
    25+            LOG_EVENT(fmt, local_name, __VA_ARGS__);                    \
    26+            local_event();                                              \
    27+        });                                                             \
    28     } while (0)
    29 
    30 #define LOG_EVENT(fmt, ...) \
    31@@ -179,7 +181,7 @@ void ValidationSignals::UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlo
    32     auto event = [pindexNew, pindexFork, fInitialDownload, this] {
    33         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.UpdatedBlockTip(pindexNew, pindexFork, fInitialDownload); });
    34     };
    35-    ENQUEUE_AND_LOG_EVENT(event, "%s: new block hash=%s fork block hash=%s (in IBD=%s)", __func__,
    36+    ENQUEUE_AND_LOG_EVENT(std::move(event), "%s: new block hash=%s fork block hash=%s (in IBD=%s)", __func__,
    37                           pindexNew->GetBlockHash().ToString(),
    38                           pindexFork ? pindexFork->GetBlockHash().ToString() : "null",
    39                           fInitialDownload);
    40@@ -196,7 +198,7 @@ void ValidationSignals::TransactionAddedToMempool(const NewMempoolTransactionInf
    41     auto event = [tx, mempool_sequence, this] {
    42         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.TransactionAddedToMempool(tx, mempool_sequence); });
    43     };
    44-    ENQUEUE_AND_LOG_EVENT(event, "%s: txid=%s wtxid=%s", __func__,
    45+    ENQUEUE_AND_LOG_EVENT(std::move(event), "%s: txid=%s wtxid=%s", __func__,
    46                           tx.info.m_tx->GetHash().ToString(),
    47                           tx.info.m_tx->GetWitnessHash().ToString());
    48 }
    49@@ -205,7 +207,7 @@ void ValidationSignals::TransactionRemovedFromMempool(const CTransactionRef& tx,
    50     auto event = [tx, reason, mempool_sequence, this] {
    51         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.TransactionRemovedFromMempool(tx, reason, mempool_sequence); });
    52     };
    53-    ENQUEUE_AND_LOG_EVENT(event, "%s: txid=%s wtxid=%s reason=%s", __func__,
    54+    ENQUEUE_AND_LOG_EVENT(std::move(event), "%s: txid=%s wtxid=%s reason=%s", __func__,
    55                           tx->GetHash().ToString(),
    56                           tx->GetWitnessHash().ToString(),
    57                           RemovalReasonToString(reason));
    58@@ -217,7 +219,7 @@ void ValidationSignals::BlockConnected(const ChainstateRole& role, std::shared_p
    59     auto event = [role, pblock = std::move(pblock), pindex, this] {
    60         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.BlockConnected(role, pblock, pindex); });
    61     };
    62-    ENQUEUE_AND_LOG_EVENT(event, "%s: block hash=%s block height=%d", __func__,
    63+    ENQUEUE_AND_LOG_EVENT(std::move(event), "%s: block hash=%s block height=%d", __func__,
    64                           block_hash,
    65                           pindex->nHeight);
    66 }
    67@@ -227,7 +229,7 @@ void ValidationSignals::MempoolTransactionsRemovedForBlock(const std::vector<Rem
    68     auto event = [txs_removed_for_block, nBlockHeight, this] {
    69         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.MempoolTransactionsRemovedForBlock(txs_removed_for_block, nBlockHeight); });
    70     };
    71-    ENQUEUE_AND_LOG_EVENT(event, "%s: block height=%s txs removed=%s", __func__,
    72+    ENQUEUE_AND_LOG_EVENT(std::move(event), "%s: block height=%s txs removed=%s", __func__,
    73                           nBlockHeight,
    74                           txs_removed_for_block.size());
    75 }
    76@@ -238,7 +240,7 @@ void ValidationSignals::BlockDisconnected(std::shared_ptr<const CBlock> pblock,
    77     auto event = [pblock = std::move(pblock), pindex, this] {
    78         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.BlockDisconnected(pblock, pindex); });
    79     };
    80-    ENQUEUE_AND_LOG_EVENT(event, "%s: block hash=%s block height=%d", __func__,
    81+    ENQUEUE_AND_LOG_EVENT(std::move(event), "%s: block hash=%s block height=%d", __func__,
    82                           block_hash,
    83                           pindex->nHeight);
    84 }
    85@@ -248,7 +250,7 @@ void ValidationSignals::ChainStateFlushed(const ChainstateRole& role, const CBlo
    86     auto event = [role, locator, this] {
    87         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.ChainStateFlushed(role, locator); });
    88     };
    89-    ENQUEUE_AND_LOG_EVENT(event, "%s: block hash=%s", __func__,
    90+    ENQUEUE_AND_LOG_EVENT(std::move(event), "%s: block hash=%s", __func__,
    91                           locator.IsNull() ? "null" : locator.vHave.front().ToString());
    92 }
    93 
    

    rkrux commented at 2:34 pm on March 10, 2026:

    The block would typically be freed on the scheduler thread — but only because ConnectTrace usually went out of scope before the scheduler ran. If the scheduler ran first, the block would instead be freed on the validation thread.

    I have not been able to completely understand as to why the block would be freed on the validation thread even if the scheduler ran first.

    From what I understand: these two threads are executing in parallel (on multicore systems) and it’s highly likely that the block in the caller in the validation thread would go out of scope before the corresponding event handler function finishes in the scheduler thread, making the scheduler thread responsible for freeing the block.


    sedited commented at 3:59 pm on March 10, 2026:
    Yes, your description is what is happening currently. The change here just enforces existing behavior to ensure that a developer reading the code understands this and protects against regressions of it in case a future change seeks to extend the lifetime of the block in the thread triggering the validation signal.
  5. stickies-v commented at 7:33 am on March 2, 2026: contributor

    Concept ACK

    I think the current commit message wordings are not very easy to understand. Best-effort attempt at an alternative for the first one:

     0validation: Move block into BlockConnected signal
     1
     2Transfer unique ownership of the block's shared_ptr from the
     3validation thread to the scheduler thread through std::move, rather
     4than copying the shared_ptr and relying on refcount timing to
     5determine which thread deallocates the block.
     6
     7Previously, both the caller (via ConnectTrace) and the queued event
     8lambda held copies of the shared_ptr. The block would typically be
     9freed on the scheduler thread  but only because ConnectTrace usually
    10went out of scope before the scheduler ran. If the scheduler ran
    11first, the block would instead be freed on the validation thread.
    12
    13Now, ownership is transferred at each step: ConnectTrace yields via
    14std::move, BlockConnected takes by value, and the event lambda
    15move-captures the shared_ptr. This guarantees the block is always
    16freed on the scheduler thread, avoiding potentially expensive
    17deallocation on the validation thread.
    

    Also, I think it would be natural for this PR to update ActivateBestChainStep to move the block into the connect trace?

     0diff --git a/src/validation.cpp b/src/validation.cpp
     1index df26ef23cc..d824044443 100644
     2--- a/src/validation.cpp
     3+++ b/src/validation.cpp
     4@@ -3219,7 +3219,7 @@ void Chainstate::PruneBlockIndexCandidates() {
     5  *
     6  * [@returns](/bitcoin-bitcoin/contributor/returns/) true unless a system error occurred
     7  */
     8-bool Chainstate::ActivateBestChainStep(BlockValidationState& state, CBlockIndex* pindexMostWork, const std::shared_ptr<const CBlock>& pblock, bool& fInvalidFound, ConnectTrace& connectTrace)
     9+bool Chainstate::ActivateBestChainStep(BlockValidationState& state, CBlockIndex* pindexMostWork, std::shared_ptr<const CBlock> pblock, bool& fInvalidFound, ConnectTrace& connectTrace)
    10 {
    11     AssertLockHeld(cs_main);
    12     if (m_mempool) AssertLockHeld(m_mempool->cs);
    13@@ -3264,7 +3264,7 @@ bool Chainstate::ActivateBestChainStep(BlockValidationState& state, CBlockIndex*
    14 
    15         // Connect new blocks.
    16         for (CBlockIndex* pindexConnect : vpindexToConnect | std::views::reverse) {
    17-            if (!ConnectTip(state, pindexConnect, pindexConnect == pindexMostWork ? pblock : std::shared_ptr<const CBlock>(), connectTrace, disconnectpool)) {
    18+            if (!ConnectTip(state, pindexConnect, pindexConnect == pindexMostWork ? std::move(pblock) : std::shared_ptr<const CBlock>(), connectTrace, disconnectpool)) {
    19                 if (state.IsInvalid()) {
    20                     // The block violates a consensus rule.
    21                     if (state.GetResult() != BlockValidationResult::BLOCK_MUTATED) {
    22@@ -3414,7 +3414,7 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr<
    23                 // in case snapshot validation is completed during ActivateBestChainStep, the
    24                 // result of GetRole() changes from BACKGROUND to NORMAL.
    25                const ChainstateRole chainstate_role{this->GetRole()};
    26-                if (!ActivateBestChainStep(state, pindexMostWork, pblock && pblock->GetHash() == pindexMostWork->GetBlockHash() ? pblock : nullBlockPtr, fInvalidFound, connectTrace)) {
    27+                if (!ActivateBestChainStep(state, pindexMostWork, pblock && pblock->GetHash() == pindexMostWork->GetBlockHash() ? std::move(pblock) : nullBlockPtr, fInvalidFound, connectTrace)) {
    28                     // A system error occurred
    29                     return false;
    30                 }
    31diff --git a/src/validation.h b/src/validation.h
    32index 482772c0d6..d3ad21c9ac 100644
    33--- a/src/validation.h
    34+++ b/src/validation.h
    35@@ -847,7 +847,7 @@ public:
    36     std::pair<int, int> GetPruneRange(int last_height_can_prune) const EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
    37 
    38 protected:
    39-    bool ActivateBestChainStep(BlockValidationState& state, CBlockIndex* pindexMostWork, const std::shared_ptr<const CBlock>& pblock, bool& fInvalidFound, ConnectTrace& connectTrace) EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_mempool->cs);
    40+    bool ActivateBestChainStep(BlockValidationState& state, CBlockIndex* pindexMostWork, std::shared_ptr<const CBlock> pblock, bool& fInvalidFound, ConnectTrace& connectTrace) EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_mempool->cs);
    41     bool ConnectTip(
    42         BlockValidationState& state,
    43         CBlockIndex* pindexNew,
    
  6. sedited commented at 8:00 am on March 2, 2026: contributor

    Re #34704#pullrequestreview-3874178574

    Also, I think it would be natural for this PR to update ActivateBestChainStep to move the block into the connect trace?

    When we call ActivateBestChain we take a copy of a block, meaning we in all likelihood increment its refcount from 1 to 2. So we are just passing around a block whose lifetime is already ambiguous. I’m guessing your motivation is that this would allow us to skip one additional refcount increment when we are passing it to ConnectTip? Moving in the nested while loop in ActivateBestChain also seems dangerous (though I guess the ternary condition protects against that).

  7. mzumsande commented at 9:05 am on March 2, 2026: contributor

    The destructor for blocks runs mostly in the scheduler thread.

    When we call ActivateBestChain we take a copy of a block, meaning we in all likelihood increment its refcount from 1 to 2.

    This is just the case during IBD when NewPoWValidBlock signals aren’t sent, right? I would expect that when synced to the tip, m_most_recent_block would keep pointing to the block, and the block would be destroyed whenever the next block arrives, minutes after connection. I feel like this should be mentioned somewhere.

  8. sedited commented at 9:11 am on March 2, 2026: contributor

    This is just the case during IBD when NewPoWValidBlock signals aren’t sent, right? I would expect that when synced to the tip, m_most_recent_block would keep pointing to the block, and the block would be destroyed whenever the next block arrives, minutes after connection. I feel like this should be mentioned somewhere.

    That is my read too. Will make this explicit in the PR description.

  9. sedited force-pushed on Mar 2, 2026
  10. sedited commented at 9:56 am on March 2, 2026: contributor

    Updated 76d1bc9bd3881da73c0691404b3fda7799d3214b -> 40c519bd7be3944fcb5b8c1d4eddcff8f6bb19f3 (block_connected_move_0 -> block_connected_move_1, compare)

  11. stickies-v approved
  12. stickies-v commented at 1:38 pm on March 2, 2026: contributor

    ACK 40c519bd7be3944fcb5b8c1d4eddcff8f6bb19f3

    I’m guessing your motivation is that this would allow us to skip one additional refcount increment when we are passing it to ConnectTip?

    Yeah, that’s what triggered me to look into it, but…

    Moving in the nested while loop in ActivateBestChain also seems dangerous (though I guess the ternary condition protects against that).

    I agree, I missed that, better to leave as-is.

  13. arejula27 commented at 3:55 pm on March 6, 2026: none

    ACK [40c519bd7be3944fcb5b8c1d4eddcff8f6bb19f3]

    I like the removal of an unnecessary shared ownership. With the move, the reference count never needs to increment just to be decremented once iterations ends.

    Not part of your changes, but while reviewing the PR I noticed a typo in src/validationinterface.h at lines 111–112, there is a duplicated as:

    0* Notifies listeners of transactions removed from the mempool as
    1* as a result of new block being connected. 
    

    Since you already edited the file, would you mind fixing it here? Opening a separate PR myself feels a bit like sneaking into the contributors list on a one-word fix.

  14. frankomosh commented at 5:42 am on March 12, 2026: contributor

    CrACK 40c519bd7be3944fcb5b8c1d4eddcff8f6bb19f3.

    When #34708 goes in, the std::move at the BlockConnected call site gets replaced by Assert(block) under a const auto& binding. Would this regress the ownership transfer that changes in this PR aim to establish?`

  15. rkrux commented at 8:33 am on March 12, 2026: contributor

    When #34708 goes in, the std::move at the BlockConnected call site gets replaced by Assert(block) under a const auto& binding. Would this regress the ownership transfer that changes in this PR aim to establish?`

    I suppose the other PR will rebase over master when this PR is merged, accommodating the move changes of this PR in that line.

  16. sedited force-pushed on Mar 12, 2026
  17. sedited commented at 11:12 am on March 12, 2026: contributor

    Rebased 40c519bd7be3944fcb5b8c1d4eddcff8f6bb19f3 -> 95e3359967e1c2ae7147867e4bae6c317f0dbfc8 (block_connected_move_1 -> block_connected_move_2, compare)

    I suppose the other PR will rebase over master when this PR is merged, accommodating the move changes of this PR in that line.

    Or the other way around :)

  18. frankomosh commented at 11:23 am on March 12, 2026: contributor
    Re-ACK 95e3359967e1c2ae7147867e4bae6c317f0dbfc8
  19. DrahtBot requested review from stickies-v on Mar 12, 2026
  20. stickies-v commented at 2:48 pm on March 12, 2026: contributor

    re-ACK 95e3359967e1c2ae7147867e4bae6c317f0dbfc8, no changes except addressing merge conflict

    nit: dabdcfaa59c48c4c81ca03c3c283b33bce7de375 commit message still references ConnectTrace

  21. rkrux commented at 10:00 am on March 13, 2026: contributor

    The destructor for blocks runs mostly in the scheduler thread.

    I’ve been analysing the flamegraph to verify this claim. I did notice around ~50 biilion samples of libc.so::cfree calls in the scheduler thread and didn’t find enough in the msghand thread. However, I did find ~5 billion samples of the cfree calls in msghand but they were on top of ConnectBlock - don’t think it’s related to deallocation of the block.

    edit: I had expected to find some on top of CConman::ThreadMessageHandler because thats where the reference count of CBlock is init’ed to 1 (via ProcessMessage) but didn’t - either these are not there in significant amount or maybe my expectation is incorrect.

    Regardless, these observations don’t seem contradictory to what’s mentioned in the PR description and commits.

  22. rkrux commented at 1:42 pm on March 13, 2026: contributor

    ACK 95e3359967e1c2ae7147867e4bae6c317f0dbfc8

    I do like the explicit intent this PR conveys. I did some analysis of the shared flamegraph and tracked the shared pointer usage & copies in the block connected & disconnected flows - while the latter is quite straightforward, the former is more involved.

  23. in src/validationinterface.cpp:168 in 129c598dae outdated
    172+        static_assert(std::is_rvalue_reference_v<decltype((event))>,    \
    173+                      "event must be passed as an rvalue");             \
    174+        auto local_name = (name);                                       \
    175+        LOG_EVENT("Enqueuing " fmt, local_name, __VA_ARGS__);           \
    176+        m_internals->m_task_runner->insert([=, local_event = (event)] { \
    177+            LOG_EVENT(fmt, local_name, __VA_ARGS__);                    \
    


    maflcko commented at 11:03 am on March 16, 2026:

    nit in 129c598dae59899af4f923f4cec20a325d2df56e: Completely unrelated, because you didn’t change this line, but it seems a bit wasteful to create a full copy of all args used for logging, when the local event already has created a copy of them (or moved them into the local event).

    This is easy to see, because if any of the args were non-copyable, the compilation would fail. E.g.:

     0diff --git a/src/validationinterface.cpp b/src/validationinterface.cpp
     1index c7be6abc3a..6262a60342 100644
     2--- a/src/validationinterface.cpp
     3+++ b/src/validationinterface.cpp
     4@@ -174,2 +174,3 @@ void ValidationSignals::SyncWithValidationInterfaceQueue()
     5 void ValidationSignals::UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload) {
     6+    const CBlockIndex& new_index{*pindexNew};
     7     // Dependencies exist that require UpdatedBlockTip events to be delivered in the order in which
     8@@ -182,3 +183,3 @@ void ValidationSignals::UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlo
     9     ENQUEUE_AND_LOG_EVENT(event, "%s: new block hash=%s fork block hash=%s (in IBD=%s)", __func__,
    10-                          pindexNew->GetBlockHash().ToString(),
    11+                          new_index.GetBlockHash().ToString(),
    12                           pindexFork ? pindexFork->GetBlockHash().ToString() : "null",
    

    I don’t see a trivial way to fix this. I guess the second LOG_EVENT would have to be moved into the body of each auto event = ... lambda?

    Maybe a tracking issue could be opened for this.

    This commit seems like an improvement either way. E.g. reducing three copies of the full txs_removed_for_block vector to two copies:

    • First copy into auto event = [txs_removed_for_block, ...
    • Then create a copy, by copying the event in m_internals->m_task_runner->insert([=] (this commit replaces it with a move)
    • Then create another copy (for the LOG_EVENT) in the same m_internals->m_task_runner->insert([=] call. (This copy remains after this commit.

    sedited commented at 1:41 pm on March 16, 2026:

    Yeah, I was annoyed when I noticed this too. How about conditionally formatting the string, and then moving those around?

     0diff --git a/src/validationinterface.cpp b/src/validationinterface.cpp
     1index 39bb405426..2db726c252 100644
     2--- a/src/validationinterface.cpp
     3+++ b/src/validationinterface.cpp
     4@@ -161,10 +161,13 @@ void ValidationSignals::SyncWithValidationInterfaceQueue()
     5-#define ENQUEUE_AND_LOG_EVENT(event, fmt, name, ...)                    \
     6-    do {                                                                \
     7-        static_assert(std::is_rvalue_reference_v<decltype((event))>,    \
     8-                      "event must be passed as an rvalue");             \
     9-        auto local_name = (name);                                       \
    10-        LOG_EVENT("Enqueuing " fmt, local_name, __VA_ARGS__);           \
    11-        m_internals->m_task_runner->insert([=, local_event = (event)] { \
    12-            LOG_EVENT(fmt, local_name, __VA_ARGS__);                    \
    13-            local_event();                                              \
    14-        });                                                             \
    15+#define ENQUEUE_AND_LOG_EVENT(event, fmt, name, ...)                                               \
    16+    do {                                                                                           \
    17+        static_assert(std::is_rvalue_reference_v<decltype((event))>,                               \
    18+                      "event must be passed as an rvalue");                                        \
    19+        auto local_name = (name);                                                                  \
    20+        auto log_msg = ShouldLog(BCLog::VALIDATION, BCLog::Level::Debug)                           \
    21+            ? tfm::format(fmt, local_name, __VA_ARGS__)                                            \
    22+            : std::string{};                                                                       \
    23+        LogDebug(BCLog::VALIDATION, "Enqueuing %s", log_msg);                                      \
    24+        m_internals->m_task_runner->insert([log_msg = std::move(log_msg), local_event = (event)] { \
    25+            LogDebug(BCLog::VALIDATION, "%s", log_msg);                                            \
    26+            local_event();                                                                         \
    27+        });                                                                                        \
    28@@ -173,3 +175,0 @@ void ValidationSignals::SyncWithValidationInterfaceQueue()
    29-#define LOG_EVENT(fmt, ...) \
    30-    LogDebug(BCLog::VALIDATION, fmt "\n", __VA_ARGS__)
    31-
    32@@ -192 +192 @@ void ValidationSignals::ActiveTipChange(const CBlockIndex& new_tip, bool is_ibd)
    33-    LOG_EVENT("%s: new block hash=%s block height=%d", __func__, new_tip.GetBlockHash().ToString(), new_tip.nHeight);
    34+    LogDebug(BCLog::VALIDATION, "%s: new block hash=%s block height=%d", __func__, new_tip.GetBlockHash().ToString(), new_tip.nHeight);
    35@@ -218 +218 @@ void ValidationSignals::BlockConnected(const ChainstateRole& role, std::shared_p
    36-    auto block_hash{pblock->GetHash().ToString()};
    37+    std::string block_hash = ShouldLog(BCLog::VALIDATION, BCLog::Level::Debug) ? pblock->GetHash().ToString() : "";
    38@@ -239 +239 @@ void ValidationSignals::BlockDisconnected(std::shared_ptr<const CBlock> pblock,
    39-    auto block_hash{pblock->GetHash().ToString()};
    40+    std::string block_hash = ShouldLog(BCLog::VALIDATION, BCLog::Level::Debug) ? pblock->GetHash().ToString() : "";
    41@@ -259 +259 @@ void ValidationSignals::BlockChecked(const std::shared_ptr<const CBlock>& block,
    42-    LOG_EVENT("%s: block hash=%s state=%s", __func__,
    43+    LogDebug(BCLog::VALIDATION, "%s: block hash=%s state=%s", __func__,
    44@@ -265 +265 @@ void ValidationSignals::NewPoWValidBlock(const CBlockIndex *pindex, const std::s
    45-    LOG_EVENT("%s: block hash=%s", __func__, block->GetHash().ToString());
    46+    LogDebug(BCLog::VALIDATION, "%s: block hash=%s", __func__, block->GetHash().ToString());
    

    maflcko commented at 2:39 pm on March 16, 2026:

    Oh yeah, that looks nice. However, some nits:

    • I think it is still using block_hash, which can be dropped from the other commits (see the other comments on those commits)
    • I think keeping #define LOG_EVENT(fmt, ...) \ could still make sense, so that the non-background callbacks can just use it.
    • Also, the background callback can continue to use LOG_EVENT("%s", log_msg)?
    • Simply removing the trailing "\n" in LOG_EVENT should be fine as a cleanup.

    Happy to re-review if you push that.

  24. in src/validation.cpp:3394 in dabdcfaa59 outdated
    3390@@ -3391,9 +3391,9 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr<
    3391                 }
    3392                 pindexNewTip = m_chain.Tip();
    3393 
    3394-                for (const auto& [index, block] : connected_blocks) {
    3395+                for (auto& [index, block] : connected_blocks) {
    


    maflcko commented at 11:13 am on March 16, 2026:

    nit in dabdcfaa59c48c4c81ca03c3c283b33bce7de375: Should this not be

    0                for (auto& [index, block] : std::move(connected_blocks)) {
    

    Otherwise, clang-tidy use-after-move won’t see that the content of the vector was moved?

    Also, unrelated: I don’t understand the comment on std::vector<ConnectedBlock> connected_blocks; // Destructed before cs_main is unlocked. I guess it is not wrong, but I fail to see how it is relevant.

    Also, unrelated: I don’t understand the comment // Lock transaction pool for at least as long as it takes for connected_blocks to be consumed. I guess this should have been removed in #34708?

    Also, unrelated: it could make sense to reduce the scope either in this commit or in a new commit:

     0diff --git a/src/validation.cpp b/src/validation.cpp
     1index 1efe109b6f..fedc3d84a3 100644
     2--- a/src/validation.cpp
     3+++ b/src/validation.cpp
     4@@ -3356,3 +3356,2 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr<
     5             {
     6-            // Lock transaction pool for at least as long as it takes for connected_blocks to be consumed
     7             LOCK(MempoolMutex());
     8@@ -3364,3 +3363,2 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr<
     9                 // (with the exception of shutdown due to hardware issues, low disk space, etc).
    10-                std::vector<ConnectedBlock> connected_blocks; // Destructed before cs_main is unlocked
    11 
    12@@ -3381,2 +3379,3 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr<
    13                const ChainstateRole chainstate_role{this->GetRole()};
    14+                std::vector<ConnectedBlock> connected_blocks;
    15                 if (!ActivateBestChainStep(state, pindexMostWork, pblock && pblock->GetHash() == pindexMostWork->GetBlockHash() ? pblock : nullBlockPtr, fInvalidFound, connected_blocks)) {
    

    maflcko commented at 10:40 am on March 18, 2026:
    Happy to push the second unrelated diff as a separate pull request. I shouldn’t conflict, I guess.
  25. in src/validationinterface.cpp:218 in dabdcfaa59 outdated
    212@@ -213,13 +213,14 @@ void ValidationSignals::TransactionRemovedFromMempool(const CTransactionRef& tx,
    213                           RemovalReasonToString(reason));
    214 }
    215 
    216-void ValidationSignals::BlockConnected(const ChainstateRole& role, const std::shared_ptr<const CBlock>& pblock, const CBlockIndex* pindex)
    217+void ValidationSignals::BlockConnected(const ChainstateRole& role, std::shared_ptr<const CBlock> pblock, const CBlockIndex* pindex)
    218 {
    219-    auto event = [role, pblock, pindex, this] {
    220+    auto block_hash{pblock->GetHash().ToString()};
    


    maflcko commented at 11:19 am on March 16, 2026:

    nit in https://github.com/bitcoin/bitcoin/commit/dabdcfaa59c48c4c81ca03c3c283b33bce7de375: I guess you are doing this to avoid a third copy of pblock, and instead do a copy of block_hash now? Not sure which one is better. I think it may be best to leave this as-is and create a redundant third copy of pblock, until the logging is fixed to not create any new copies at all.

    Otherwise, when the logging is eventually fixed, there will be a possibly redundant calculation and copy of block_hash.


    sedited commented at 4:29 pm on March 16, 2026:
    I think I prefer keeping this (with the changes to only calculate if we are indeed logging). Copying the block instead would leave it to be destructed at the end of the function’s scope, which imo runs counter to the PRs goals. Or do you have a different approach in mind that could retain that?

    maflcko commented at 9:17 am on March 17, 2026:

    Sry, I actually missed that the block is moved into the event first, before the block hash is used to construct the log message. So I guess the copy of the hash is needed, unless you want to restructure how the log message is constructed.

    I’ve implemented that, and while it is a minimally more verbose, I think it is overall nicer, because:

    • Async events and sync events equally format the log message as the first step. Async ones via LOG_MSG and sync ones via LOG_EVENT
    • There is no need to duplicate the ShouldLog(BCLog::VALIDATION, BCLog::Level::Debug) ? into several functions and macros. Only one place does this thing what feels a bit like a layer violation
    • The ENQUEUE_AND_LOG_EVENT macro is easier to follow, because it no longer deals with __VA_ARGS__ formatting at all, and only has two arguments.

    Here is the diff on top of this pull request. (Note that I’ve only compiled it with clang-tidy -p=./bld-cmake/ src/validationinterface.cpp and I have not run clang-format yet. If you agree with the diff, it could make sense to apply it on top of the first commit instead of the top of this pull).

      0diff --git a/src/validationinterface.cpp b/src/validationinterface.cpp
      1index 202eac4ff3..381f9dd870 100644
      2--- a/src/validationinterface.cpp
      3+++ b/src/validationinterface.cpp
      4@@ -159,4 +159,3 @@ void ValidationSignals::SyncWithValidationInterfaceQueue()
      5 //
      6-// NOTE: The lambda captures the event by move
      7-#define ENQUEUE_AND_LOG_EVENT(event, fmt, name, ...)                                               \
      8+#define ENQUEUE_AND_LOG_EVENT(event, log_msg) \
      9     do {                                                                                           \
     10@@ -164,9 +163,7 @@ void ValidationSignals::SyncWithValidationInterfaceQueue()
     11                       "event must be passed as an rvalue");                                        \
     12-        auto local_name = (name);                                                                  \
     13-        auto log_msg = ShouldLog(BCLog::VALIDATION, BCLog::Level::Debug)                           \
     14-            ? tfm::format(fmt, local_name, __VA_ARGS__)                                            \
     15-            : std::string{};                                                                       \
     16+        static_assert(!std::is_const_v<std::remove_reference_t<decltype((log_msg))>>,              \
     17+                      "log_msg must be mutable reference");\
     18         LOG_EVENT("Enqueuing %s", log_msg);                                                        \
     19-        m_internals->m_task_runner->insert([log_msg = std::move(log_msg), local_event = (event)] { \
     20-            LOG_EVENT("%s", log_msg);                                                              \
     21+        m_internals->m_task_runner->insert([local_log_msg = std::move(log_msg), local_event = (event)] { \
     22+            LOG_EVENT("%s", local_log_msg);                                                              \
     23             local_event();                                                                         \
     24@@ -175,2 +172,8 @@ void ValidationSignals::SyncWithValidationInterfaceQueue()
     25 
     26+#define LOG_MSG(fmt, ...) \
     27+    (        ShouldLog(BCLog::VALIDATION, BCLog::Level::Debug)                           \
     28+            ? tfm::format((fmt), __VA_ARGS__)                                            \
     29+            : std::string{}                                                                       \
     30+    )
     31+
     32 #define LOG_EVENT(fmt, ...) \
     33@@ -183,6 +186,3 @@ void ValidationSignals::UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlo
     34 
     35-    auto event = [pindexNew, pindexFork, fInitialDownload, this] {
     36-        m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.UpdatedBlockTip(pindexNew, pindexFork, fInitialDownload); });
     37-    };
     38-    ENQUEUE_AND_LOG_EVENT(std::move(event), "%s: new block hash=%s fork block hash=%s (in IBD=%s)", __func__,
     39+    auto log_msg = LOG_MSG(    "%s: new block hash=%s fork block hash=%s (in IBD=%s)", __func__,
     40                           pindexNew->GetBlockHash().ToString(),
     41@@ -190,2 +190,6 @@ void ValidationSignals::UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlo
     42                           fInitialDownload);
     43+    auto event = [pindexNew, pindexFork, fInitialDownload, this] {
     44+        m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.UpdatedBlockTip(pindexNew, pindexFork, fInitialDownload); });
     45+    };
     46+    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
     47 }
     48@@ -200,2 +204,5 @@ void ValidationSignals::TransactionAddedToMempool(const NewMempoolTransactionInf
     49 {
     50+    auto log_msg = LOG_MSG(    "%s: txid=%s wtxid=%s", __func__,
     51+                          tx.info.m_tx->GetHash().ToString(),
     52+                          tx.info.m_tx->GetWitnessHash().ToString());
     53     auto event = [tx, mempool_sequence, this] {
     54@@ -203,5 +210,3 @@ void ValidationSignals::TransactionAddedToMempool(const NewMempoolTransactionInf
     55     };
     56-    ENQUEUE_AND_LOG_EVENT(std::move(event), "%s: txid=%s wtxid=%s", __func__,
     57-                          tx.info.m_tx->GetHash().ToString(),
     58-                          tx.info.m_tx->GetWitnessHash().ToString());
     59+    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
     60 }
     61@@ -209,6 +214,3 @@ void ValidationSignals::TransactionAddedToMempool(const NewMempoolTransactionInf
     62 void ValidationSignals::TransactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason, uint64_t mempool_sequence) {
     63-    auto event = [tx, reason, mempool_sequence, this] {
     64-        m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.TransactionRemovedFromMempool(tx, reason, mempool_sequence); });
     65-    };
     66-    ENQUEUE_AND_LOG_EVENT(std::move(event), "%s: txid=%s wtxid=%s reason=%s", __func__,
     67+    auto log_msg = LOG_MSG(    "%s: txid=%s wtxid=%s reason=%s", __func__,
     68                           tx->GetHash().ToString(),
     69@@ -216,2 +218,6 @@ void ValidationSignals::TransactionRemovedFromMempool(const CTransactionRef& tx,
     70                           RemovalReasonToString(reason));
     71+    auto event = [tx, reason, mempool_sequence, this] {
     72+        m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.TransactionRemovedFromMempool(tx, reason, mempool_sequence); });
     73+    };
     74+    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
     75 }
     76@@ -220,3 +226,5 @@ void ValidationSignals::BlockConnected(const ChainstateRole& role, std::shared_p
     77 {
     78-    std::string block_hash = ShouldLog(BCLog::VALIDATION, BCLog::Level::Debug) ? pblock->GetHash().ToString() : "";
     79+    auto log_msg = LOG_MSG(    "%s: block hash=%s block height=%d", __func__,
     80+                          pblock->GetHash().ToString(),
     81+                          pindex->nHeight);
     82     auto event = [role, pblock = std::move(pblock), pindex, this] {
     83@@ -224,5 +232,3 @@ void ValidationSignals::BlockConnected(const ChainstateRole& role, std::shared_p
     84     };
     85-    ENQUEUE_AND_LOG_EVENT(std::move(event), "%s: block hash=%s block height=%d", __func__,
     86-                          block_hash,
     87-                          pindex->nHeight);
     88+    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
     89 }
     90@@ -231,2 +237,5 @@ void ValidationSignals::MempoolTransactionsRemovedForBlock(const std::vector<Rem
     91 {
     92+    auto log_msg = LOG_MSG(    "%s: block height=%s txs removed=%s", __func__,
     93+                          nBlockHeight,
     94+                          txs_removed_for_block.size());
     95     auto event = [txs_removed_for_block, nBlockHeight, this] {
     96@@ -234,5 +243,3 @@ void ValidationSignals::MempoolTransactionsRemovedForBlock(const std::vector<Rem
     97     };
     98-    ENQUEUE_AND_LOG_EVENT(std::move(event), "%s: block height=%s txs removed=%s", __func__,
     99-                          nBlockHeight,
    100-                          txs_removed_for_block.size());
    101+    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
    102 }
    103@@ -241,3 +248,5 @@ void ValidationSignals::BlockDisconnected(std::shared_ptr<const CBlock> pblock,
    104 {
    105-    std::string block_hash = ShouldLog(BCLog::VALIDATION, BCLog::Level::Debug) ? pblock->GetHash().ToString() : "";
    106+    auto log_msg = LOG_MSG(    "%s: block hash=%s block height=%d", __func__,
    107+                          pblock->GetHash().ToString(),
    108+                          pindex->nHeight);
    109     auto event = [pblock = std::move(pblock), pindex, this] {
    110@@ -245,5 +254,3 @@ void ValidationSignals::BlockDisconnected(std::shared_ptr<const CBlock> pblock,
    111     };
    112-    ENQUEUE_AND_LOG_EVENT(std::move(event), "%s: block hash=%s block height=%d", __func__,
    113-                          block_hash,
    114-                          pindex->nHeight);
    115+    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
    116 }
    117@@ -252,2 +259,4 @@ void ValidationSignals::ChainStateFlushed(const ChainstateRole& role, const CBlo
    118 {
    119+    auto log_msg = LOG_MSG(    "%s: block hash=%s", __func__,
    120+                          locator.IsNull() ? "null" : locator.vHave.front().ToString());
    121     auto event = [role, locator, this] {
    122@@ -255,4 +264,3 @@ void ValidationSignals::ChainStateFlushed(const ChainstateRole& role, const CBlo
    123     };
    124-    ENQUEUE_AND_LOG_EVENT(std::move(event), "%s: block hash=%s", __func__,
    125-                          locator.IsNull() ? "null" : locator.vHave.front().ToString());
    126+    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
    127 }
    

    stickies-v commented at 11:31 am on March 17, 2026:

    Is it important that we log the entire message when enqueueing? Because if not, and we log e.g. just the function name, I think we don’t really need any of these macros?

      0diff --git a/src/validationinterface.cpp b/src/validationinterface.cpp
      1index cc1abcd6cd..038e9d220b 100644
      2--- a/src/validationinterface.cpp
      3+++ b/src/validationinterface.cpp
      4@@ -154,123 +154,100 @@ void ValidationSignals::SyncWithValidationInterfaceQueue()
      5     promise.get_future().wait();
      6 }
      7 
      8-// Use a macro instead of a function for conditional logging to prevent
      9-// evaluating arguments when logging is not enabled.
     10-//
     11-// NOTE: The lambda captures the event by move
     12-#define ENQUEUE_AND_LOG_EVENT(event, log_msg)                                                            \
     13-    do {                                                                                                 \
     14-        static_assert(std::is_rvalue_reference_v<decltype((event))>,                                     \
     15-                      "event must be passed as an rvalue");                                              \
     16-        static_assert(!std::is_const_v<std::remove_reference_t<decltype((log_msg))>>,                    \
     17-                      "log_msg must be mutable reference");                                              \
     18-        LOG_EVENT("Enqueuing %s", log_msg);                                                              \
     19-        m_internals->m_task_runner->insert([local_log_msg = std::move(log_msg), local_event = (event)] { \
     20-            LOG_EVENT("%s", local_log_msg);                                                              \
     21-            local_event();                                                                               \
     22-        });                                                                                              \
     23-    } while (0)
     24-
     25-#define LOG_MSG(fmt, ...) \
     26-    (ShouldLog(BCLog::VALIDATION, BCLog::Level::Debug) ? tfm::format((fmt), __VA_ARGS__) : std::string{})
     27-
     28-#define LOG_EVENT(fmt, ...) \
     29-    LogDebug(BCLog::VALIDATION, fmt, __VA_ARGS__)
     30-
     31 void ValidationSignals::UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload) {
     32     // Dependencies exist that require UpdatedBlockTip events to be delivered in the order in which
     33     // the chain actually updates. One way to ensure this is for the caller to invoke this signal
     34     // in the same critical section where the chain is updated
     35 
     36-    auto log_msg = LOG_MSG("%s: new block hash=%s fork block hash=%s (in IBD=%s)", __func__,
     37-                          pindexNew->GetBlockHash().ToString(),
     38-                          pindexFork ? pindexFork->GetBlockHash().ToString() : "null",
     39-                          fInitialDownload);
     40-    auto event = [pindexNew, pindexFork, fInitialDownload, this] {
     41+    LogDebug(BCLog::VALIDATION, "Enqueuing %s event", __func__);
     42+    m_internals->m_task_runner->insert([pindexNew, pindexFork, fInitialDownload, this] {
     43+        LogDebug(BCLog::VALIDATION, "%s: new block hash=%s fork block hash=%s (in IBD=%s)", __func__,
     44+                 pindexNew->GetBlockHash().ToString(),
     45+                 pindexFork ? pindexFork->GetBlockHash().ToString() : "null",
     46+                 fInitialDownload);
     47         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.UpdatedBlockTip(pindexNew, pindexFork, fInitialDownload); });
     48-    };
     49-    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
     50+    });
     51 }
     52 
     53 void ValidationSignals::ActiveTipChange(const CBlockIndex& new_tip, bool is_ibd)
     54 {
     55-    LOG_EVENT("%s: new block hash=%s block height=%d", __func__, new_tip.GetBlockHash().ToString(), new_tip.nHeight);
     56+    LogDebug(BCLog::VALIDATION, "%s: new block hash=%s block height=%d", __func__, new_tip.GetBlockHash().ToString(), new_tip.nHeight);
     57     m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.ActiveTipChange(new_tip, is_ibd); });
     58 }
     59 
     60 void ValidationSignals::TransactionAddedToMempool(const NewMempoolTransactionInfo& tx, uint64_t mempool_sequence)
     61 {
     62-    auto log_msg = LOG_MSG("%s: txid=%s wtxid=%s", __func__,
     63-                          tx.info.m_tx->GetHash().ToString(),
     64-                          tx.info.m_tx->GetWitnessHash().ToString());
     65-    auto event = [tx, mempool_sequence, this] {
     66+    LogDebug(BCLog::VALIDATION, "Enqueuing %s event", __func__);
     67+    m_internals->m_task_runner->insert([tx, mempool_sequence, this] {
     68+        LogDebug(BCLog::VALIDATION, "%s: txid=%s wtxid=%s", __func__,
     69+                 tx.info.m_tx->GetHash().ToString(),
     70+                 tx.info.m_tx->GetWitnessHash().ToString());
     71         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.TransactionAddedToMempool(tx, mempool_sequence); });
     72-    };
     73-    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
     74+    });
     75 }
     76 
     77 void ValidationSignals::TransactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason, uint64_t mempool_sequence) {
     78-    auto log_msg = LOG_MSG("%s: txid=%s wtxid=%s reason=%s", __func__,
     79-                          tx->GetHash().ToString(),
     80-                          tx->GetWitnessHash().ToString(),
     81-                          RemovalReasonToString(reason));
     82-    auto event = [tx, reason, mempool_sequence, this] {
     83+    LogDebug(BCLog::VALIDATION, "Enqueuing %s event", __func__);
     84+    m_internals->m_task_runner->insert([tx, reason, mempool_sequence, this] {
     85+        LogDebug(BCLog::VALIDATION, "%s: txid=%s wtxid=%s reason=%s", __func__,
     86+                 tx->GetHash().ToString(),
     87+                 tx->GetWitnessHash().ToString(),
     88+                 RemovalReasonToString(reason));
     89         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.TransactionRemovedFromMempool(tx, reason, mempool_sequence); });
     90-    };
     91-    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
     92+    });
     93 }
     94 
     95 void ValidationSignals::BlockConnected(const ChainstateRole& role, std::shared_ptr<const CBlock> pblock, const CBlockIndex* pindex)
     96 {
     97-    auto log_msg = LOG_MSG("%s: block hash=%s block height=%d", __func__,
     98-                          pblock->GetHash().ToString(),
     99-                          pindex->nHeight);
    100-    auto event = [role, pblock = std::move(pblock), pindex, this] {
    101+    LogDebug(BCLog::VALIDATION, "Enqueuing %s event", __func__);
    102+    m_internals->m_task_runner->insert([role, pblock = std::move(pblock), pindex, this] {
    103+        LogDebug(BCLog::VALIDATION, "%s: block hash=%s block height=%d", __func__,
    104+                 pblock->GetHash().ToString(),
    105+                 pindex->nHeight);
    106         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.BlockConnected(role, pblock, pindex); });
    107-    };
    108-    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
    109+    });
    110 }
    111 
    112 void ValidationSignals::MempoolTransactionsRemovedForBlock(const std::vector<RemovedMempoolTransactionInfo>& txs_removed_for_block, unsigned int nBlockHeight)
    113 {
    114-    auto log_msg = LOG_MSG("%s: block height=%s txs removed=%s", __func__,
    115-                          nBlockHeight,
    116-                          txs_removed_for_block.size());
    117-    auto event = [txs_removed_for_block, nBlockHeight, this] {
    118+    LogDebug(BCLog::VALIDATION, "Enqueuing %s event", __func__);
    119+    m_internals->m_task_runner->insert([txs_removed_for_block, nBlockHeight, this] {
    120+        LogDebug(BCLog::VALIDATION, "%s: block height=%d txs removed=%d", __func__,
    121+                 nBlockHeight,
    122+                 txs_removed_for_block.size());
    123         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.MempoolTransactionsRemovedForBlock(txs_removed_for_block, nBlockHeight); });
    124-    };
    125-    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
    126+    });
    127 }
    128 
    129 void ValidationSignals::BlockDisconnected(std::shared_ptr<const CBlock> pblock, const CBlockIndex* pindex)
    130 {
    131-    auto log_msg = LOG_MSG("%s: block hash=%s block height=%d", __func__,
    132-                          pblock->GetHash().ToString(),
    133-                          pindex->nHeight);
    134-    auto event = [pblock = std::move(pblock), pindex, this] {
    135+    LogDebug(BCLog::VALIDATION, "Enqueuing %s event", __func__);
    136+    m_internals->m_task_runner->insert([pblock = std::move(pblock), pindex, this] {
    137+        LogDebug(BCLog::VALIDATION, "%s: block hash=%s block height=%d", __func__,
    138+                 pblock->GetHash().ToString(),
    139+                 pindex->nHeight);
    140         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.BlockDisconnected(pblock, pindex); });
    141-    };
    142-    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
    143+    });
    144 }
    145 
    146 void ValidationSignals::ChainStateFlushed(const ChainstateRole& role, const CBlockLocator& locator)
    147 {
    148-    auto log_msg = LOG_MSG("%s: block hash=%s", __func__,
    149-                          locator.IsNull() ? "null" : locator.vHave.front().ToString());
    150-    auto event = [role, locator, this] {
    151+    LogDebug(BCLog::VALIDATION, "Enqueuing %s event", __func__);
    152+    m_internals->m_task_runner->insert([role, locator, this] {
    153+        LogDebug(BCLog::VALIDATION, "%s: block hash=%s", __func__,
    154+                 locator.IsNull() ? "null" : locator.vHave.front().ToString());
    155         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.ChainStateFlushed(role, locator); });
    156-    };
    157-    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
    158+    });
    159 }
    160 
    161 void ValidationSignals::BlockChecked(const std::shared_ptr<const CBlock>& block, const BlockValidationState& state)
    162 {
    163-    LOG_EVENT("%s: block hash=%s state=%s", __func__,
    164-              block->GetHash().ToString(), state.ToString());
    165+    LogDebug(BCLog::VALIDATION, "%s: block hash=%s state=%s", __func__,
    166+             block->GetHash().ToString(), state.ToString());
    167     m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.BlockChecked(block, state); });
    168 }
    169 
    170 void ValidationSignals::NewPoWValidBlock(const CBlockIndex *pindex, const std::shared_ptr<const CBlock> &block) {
    171-    LOG_EVENT("%s: block hash=%s", __func__, block->GetHash().ToString());
    172+    LogDebug(BCLog::VALIDATION, "%s: block hash=%s", __func__, block->GetHash().ToString());
    173     m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.NewPoWValidBlock(pindex, block); });
    174 }
    

    sedited commented at 11:35 am on March 17, 2026:

    Is it important that we log the entire message when enqueueing?

    I’d say so, you probably want to match the exact event with it being processed in the callback, not just the function.


    maflcko commented at 10:39 am on March 18, 2026:

    I think for most cases, it is enough to just log when processing and a log on enqueing is not needed. However for stuff like transactions removed from mempool, it is helpful while debugging to see when the event for this txid was enqueued, and when it was executed.

    Sure, the queue is single-threaded, so one can guess the matching from the order, but this seems tedious and may break if this is ever made multi-threaded.

    So my preference would be to leave this as-is for now.


    Mechanizm101 commented at 10:44 am on March 18, 2026:
    Ok thank you…
  26. in src/validationinterface.cpp:239 in 95e3359967
    233@@ -234,13 +234,14 @@ void ValidationSignals::MempoolTransactionsRemovedForBlock(const std::vector<Rem
    234                           txs_removed_for_block.size());
    235 }
    236 
    237-void ValidationSignals::BlockDisconnected(const std::shared_ptr<const CBlock>& pblock, const CBlockIndex* pindex)
    238+void ValidationSignals::BlockDisconnected(std::shared_ptr<const CBlock> pblock, const CBlockIndex* pindex)
    239 {
    240-    auto event = [pblock, pindex, this] {
    241+    auto block_hash{pblock->GetHash().ToString()};
    


    maflcko commented at 11:21 am on March 16, 2026:
    nit in 95e3359967e1c2ae7147867e4bae6c317f0dbfc8: Same
  27. maflcko approved
  28. maflcko commented at 11:25 am on March 16, 2026: member

    lgtm. I think this is a nice change, removing some redundant copies in the first commit and documenting that the scheduler thread can act as a “garbage collector thread” in the other two commits.

    I’ve left some nits, and a follow-up idea.

    Happy to re-review if you decide to take any of the nits. They seem easy enough to be in-scope and not require a follow-up.

    review ACK 95e3359967e1c2ae7147867e4bae6c317f0dbfc8 🗯

    Signature:

    0untrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm "${path_to_any_empty_file}" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x "${path_to_this_whole_four_line_signature_blob}"
    1RUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=
    2trusted comment: review ACK 95e3359967e1c2ae7147867e4bae6c317f0dbfc8 🗯
    3kPHnbmRJ44LliSgdavOUNWwIYrCw/8+MC38SHgkWdcIlcKvEcBtV1uq3Y6rPzmlOoovPQIlFmhxHhQ1vYuHjDg==
    
  29. sedited force-pushed on Mar 16, 2026
  30. sedited commented at 5:24 pm on March 16, 2026: contributor

    Thanks for the review @maflcko,

    Updated 95e3359967e1c2ae7147867e4bae6c317f0dbfc8 -> d62c6e93a3f3777fa1227ae8775a652c315bf98d (block_connected_move_2 -> block_connected_move_3, compare)

    • Addressed @maflcko’s comment, pre-formatting the string if it will be logged to avoid copying the passed in data again.
    • Addressed @maflcko’s comment, use std::move when iterating through connected_blocks.
  31. in src/validationinterface.cpp:167 in 0227b7d6c8 outdated
    171+    do {                                                                                           \
    172+        static_assert(std::is_rvalue_reference_v<decltype((event))>,                               \
    173+                      "event must be passed as an rvalue");                                        \
    174+        auto local_name = (name);                                                                  \
    175+        auto log_msg = ShouldLog(BCLog::VALIDATION, BCLog::Level::Debug)                           \
    176+            ? tfm::format(fmt, local_name, __VA_ARGS__)                                            \
    


    maflcko commented at 8:11 am on March 17, 2026:

    nit in 0227b7d6c81f40c643d3dfd124e29fd759abc42f: I think the copy of local_name is no longer needed and can be removed:

    0            ? tfm::format((fmt), (name), __VA_ARGS__)                                            \
    

    Also, can use the () to pass args in macros (doesn’t matter here in this context, just a style nit)

  32. maflcko approved
  33. maflcko commented at 9:21 am on March 17, 2026: member

    Thx. The new push looks nice, because it also fixes the three redundant copies of all validation interface args.

    I’ve left more nits, and I am happy to re-review after them, but they are not blocking.

    lgtm d62c6e93a3f3777fa1227ae8775a652c315bf98d 🍧

    Signature:

    0untrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm "${path_to_any_empty_file}" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x "${path_to_this_whole_four_line_signature_blob}"
    1RUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=
    2trusted comment: lgtm d62c6e93a3f3777fa1227ae8775a652c315bf98d 🍧
    3m0ORBaxAhyghv4mHEwa94UOk63PsD13EDdjUKbt3DwihP/8pHAkEC5xwsmfuHCAplx7TIlxd9lI4S6Helcz9AQ==
    
  34. sedited force-pushed on Mar 17, 2026
  35. sedited commented at 10:49 am on March 17, 2026: contributor

    Thanks for the suggestion @maflcko,

    Updated d62c6e93a3f3777fa1227ae8775a652c315bf98d -> f9c9fe78182b8f1e458253037de2220130bac259 (block_connected_move_3 -> block_connected_move_4, compare)

    Updated f9c9fe78182b8f1e458253037de2220130bac259 -> 8c4439a5d13f3572c8ec9c3c5eb336e805b5cd33 (block_connected_move_4 -> block_connected_move_5, compare)

  36. sedited force-pushed on Mar 17, 2026
  37. DrahtBot added the label CI failed on Mar 17, 2026
  38. DrahtBot removed the label CI failed on Mar 17, 2026
  39. maflcko commented at 10:39 am on March 18, 2026: member

    re-review ACK 8c4439a5d13f3572c8ec9c3c5eb336e805b5cd33 🏵

    Signature:

    0untrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm "${path_to_any_empty_file}" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x "${path_to_this_whole_four_line_signature_blob}"
    1RUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=
    2trusted comment: re-review ACK 8c4439a5d13f3572c8ec9c3c5eb336e805b5cd33 🏵
    3rf+5s+7Qn1gi0Ta5aoxrSKB28/xBsKyd44leVHA5wYFbfyW0kXoeTBF4+SSGO8scM+0iG12WdJYfXj9LYa55Cg==
    
  40. DrahtBot requested review from frankomosh on Mar 18, 2026
  41. DrahtBot requested review from rkrux on Mar 18, 2026
  42. in src/validationinterface.cpp:192 in 8c4439a5d1
    201                           pindexFork ? pindexFork->GetBlockHash().ToString() : "null",
    202                           fInitialDownload);
    203+    auto event = [pindexNew, pindexFork, fInitialDownload, this] {
    204+        m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.UpdatedBlockTip(pindexNew, pindexFork, fInitialDownload); });
    205+    };
    206+    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
    


    stickies-v commented at 11:52 am on March 18, 2026:

    I think it’s really awkward that even though ENQUEUE_AND_LOG_EVENT consumes both, event must be an rvalue and log_msg must not be. Why not just require them both to be rvalue? I also think the NOTE: The lambda captures the event by move docstring is no longer relevant.

      0diff --git a/src/validationinterface.cpp b/src/validationinterface.cpp
      1index cc1abcd6cd..124040d516 100644
      2--- a/src/validationinterface.cpp
      3+++ b/src/validationinterface.cpp
      4@@ -156,19 +156,18 @@ void ValidationSignals::SyncWithValidationInterfaceQueue()
      5 
      6 // Use a macro instead of a function for conditional logging to prevent
      7 // evaluating arguments when logging is not enabled.
      8-//
      9-// NOTE: The lambda captures the event by move
     10-#define ENQUEUE_AND_LOG_EVENT(event, log_msg)                                                            \
     11-    do {                                                                                                 \
     12-        static_assert(std::is_rvalue_reference_v<decltype((event))>,                                     \
     13-                      "event must be passed as an rvalue");                                              \
     14-        static_assert(!std::is_const_v<std::remove_reference_t<decltype((log_msg))>>,                    \
     15-                      "log_msg must be mutable reference");                                              \
     16-        LOG_EVENT("Enqueuing %s", log_msg);                                                              \
     17-        m_internals->m_task_runner->insert([local_log_msg = std::move(log_msg), local_event = (event)] { \
     18-            LOG_EVENT("%s", local_log_msg);                                                              \
     19-            local_event();                                                                               \
     20-        });                                                                                              \
     21+#define ENQUEUE_AND_LOG_EVENT(event, log_msg)                                                                              \
     22+    do {                                                                                                                   \
     23+        static_assert(std::is_rvalue_reference_v<decltype((event))>,                                                       \
     24+                      "event must be passed as an rvalue");                                                                \
     25+        static_assert(std::is_rvalue_reference_v<decltype((log_msg))>,                                                     \
     26+                      "log_msg must be passed as an rvalue");                                                              \
     27+        auto enqueue_log_msg = (log_msg);                                                                                  \
     28+        LOG_EVENT("Enqueuing %s", enqueue_log_msg);                                                                        \
     29+        m_internals->m_task_runner->insert([event_log_msg = std::move(enqueue_log_msg), local_event = (event)]() mutable { \
     30+            LOG_EVENT("%s", event_log_msg);                                                                                \
     31+            local_event();                                                                                                 \
     32+        });                                                                                                                \
     33     } while (0)
     34 
     35 #define LOG_MSG(fmt, ...) \
     36@@ -189,7 +188,7 @@ void ValidationSignals::UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlo
     37     auto event = [pindexNew, pindexFork, fInitialDownload, this] {
     38         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.UpdatedBlockTip(pindexNew, pindexFork, fInitialDownload); });
     39     };
     40-    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
     41+    ENQUEUE_AND_LOG_EVENT(std::move(event), std::move(log_msg));
     42 }
     43 
     44 void ValidationSignals::ActiveTipChange(const CBlockIndex& new_tip, bool is_ibd)
     45@@ -206,10 +205,11 @@ void ValidationSignals::TransactionAddedToMempool(const NewMempoolTransactionInf
     46     auto event = [tx, mempool_sequence, this] {
     47         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.TransactionAddedToMempool(tx, mempool_sequence); });
     48     };
     49-    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
     50+    ENQUEUE_AND_LOG_EVENT(std::move(event), std::move(log_msg));
     51 }
     52 
     53-void ValidationSignals::TransactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason, uint64_t mempool_sequence) {
     54+void ValidationSignals::TransactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason, uint64_t mempool_sequence)
     55+{
     56     auto log_msg = LOG_MSG("%s: txid=%s wtxid=%s reason=%s", __func__,
     57                           tx->GetHash().ToString(),
     58                           tx->GetWitnessHash().ToString(),
     59@@ -217,7 +217,7 @@ void ValidationSignals::TransactionRemovedFromMempool(const CTransactionRef& tx,
     60     auto event = [tx, reason, mempool_sequence, this] {
     61         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.TransactionRemovedFromMempool(tx, reason, mempool_sequence); });
     62     };
     63-    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
     64+    ENQUEUE_AND_LOG_EVENT(std::move(event), std::move(log_msg));
     65 }
     66 
     67 void ValidationSignals::BlockConnected(const ChainstateRole& role, std::shared_ptr<const CBlock> pblock, const CBlockIndex* pindex)
     68@@ -228,7 +228,7 @@ void ValidationSignals::BlockConnected(const ChainstateRole& role, std::shared_p
     69     auto event = [role, pblock = std::move(pblock), pindex, this] {
     70         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.BlockConnected(role, pblock, pindex); });
     71     };
     72-    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
     73+    ENQUEUE_AND_LOG_EVENT(std::move(event), std::move(log_msg));
     74 }
     75 
     76 void ValidationSignals::MempoolTransactionsRemovedForBlock(const std::vector<RemovedMempoolTransactionInfo>& txs_removed_for_block, unsigned int nBlockHeight)
     77@@ -239,7 +239,7 @@ void ValidationSignals::MempoolTransactionsRemovedForBlock(const std::vector<Rem
     78     auto event = [txs_removed_for_block, nBlockHeight, this] {
     79         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.MempoolTransactionsRemovedForBlock(txs_removed_for_block, nBlockHeight); });
     80     };
     81-    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
     82+    ENQUEUE_AND_LOG_EVENT(std::move(event), std::move(log_msg));
     83 }
     84 
     85 void ValidationSignals::BlockDisconnected(std::shared_ptr<const CBlock> pblock, const CBlockIndex* pindex)
     86@@ -250,7 +250,7 @@ void ValidationSignals::BlockDisconnected(std::shared_ptr<const CBlock> pblock,
     87     auto event = [pblock = std::move(pblock), pindex, this] {
     88         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.BlockDisconnected(pblock, pindex); });
     89     };
     90-    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
     91+    ENQUEUE_AND_LOG_EVENT(std::move(event), std::move(log_msg));
     92 }
     93 
     94 void ValidationSignals::ChainStateFlushed(const ChainstateRole& role, const CBlockLocator& locator)
     95@@ -260,7 +260,7 @@ void ValidationSignals::ChainStateFlushed(const ChainstateRole& role, const CBlo
     96     auto event = [role, locator, this] {
     97         m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.ChainStateFlushed(role, locator); });
     98     };
     99-    ENQUEUE_AND_LOG_EVENT(std::move(event), log_msg);
    100+    ENQUEUE_AND_LOG_EVENT(std::move(event), std::move(log_msg));
    101 }
    102 
    103 void ValidationSignals::BlockChecked(const std::shared_ptr<const CBlock>& block, const BlockValidationState& state)
    
  43. DrahtBot requested review from stickies-v on Mar 18, 2026
  44. validation: Move validation signal events to task runner
    Currently arguments passed through the validation interface are copied
    three times. Once on capture in the event, again when the event itself
    is copied into the task runner lambda, and when the various arguments
    used by the logging statement are copied into the lambda.
    
    This change avoids the variables captured by the event being copied
    again. Next to avoiding needless copies, this is done in preparation of
    the following two commits, which seek to clarify the ownership semantics
    of the blocks passed through the validation interface.
    
    Co-authored-by: stickies-v <stickies-v@protonmail.com>
    8b0fb64c02
  45. validation: Move block into BlockConnected signal
    This makes existing behaviour of the block's destructor triggering on
    the scheduler thread more explicit by moving it to the thread. The
    scheduler thread doing so is useful, since it does not block the thread
    doing validation while releasing a block's memory.
    
    Previously, both the caller and the queued event lambda held copies of
    the shared_ptr. The block would typically be freed on the scheduler
    thread - but only because it went out of scope before the queued event
    on the scheduler thread ran. If the scheduler ran first, the block would
    instead be freed on the validation thread.
    
    Now, ownership is transferred at each step when invoking the
    BlockConnected signal: connected_blocks yields via std::move,
    BlockConnected takes by value, and the event lambda move-captures the
    shared_ptr. Though it is possible that this only decrements the block's
    reference count, blocks are also read from disk in `ConnectTip`, which
    now explicitly results in their memory being released on the scheduler
    thread.
    4d02d2b316
  46. validation: Move block into BlockDisconnected signal
    This makes existing behaviour of the block's destructor triggering on
    the scheduler thread more explicit by moving it to the thread. The
    scheduler thread doing so is useful, since it does not block the thread
    doing validation while releasing a block's memory.
    
    DisconnectTip already creates and destroys the block itself, so moving
    it into the validation signals is well scoped.
    d6f680b427
  47. sedited force-pushed on Mar 18, 2026
  48. sedited commented at 12:45 pm on March 18, 2026: contributor

    Updated 8c4439a5d13f3572c8ec9c3c5eb336e805b5cd33 -> d6f680b4275296e4a7f9e6ea693149e59800e495 (block_connected_move_5 -> block_connected_move_6, compare)

  49. maflcko commented at 12:52 pm on March 18, 2026: member

    re-lgtm. Only change is calling the std::string move constructor once more for each function call, which my patch skipped, but I guess it is fast and harmless. Anything should be fine here.

    re-review ACK d6f680b4275296e4a7f9e6ea693149e59800e495 🔌

    Signature:

    0untrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm "${path_to_any_empty_file}" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x "${path_to_this_whole_four_line_signature_blob}"
    1RUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=
    2trusted comment: re-review ACK d6f680b4275296e4a7f9e6ea693149e59800e495 🔌
    3CEHnyUbbBkwJ6/iA99iSBpEdEyQ3VDejMzvrL+/H8nGB3B5y0evMQCLMsqg0i/HqX8J3kor7eT/s+LLUS2d7Bw==
    
  50. stickies-v commented at 1:17 pm on March 18, 2026: contributor
    re-ACK d6f680b4275296e4a7f9e6ea693149e59800e495
  51. frankomosh commented at 3:35 pm on March 18, 2026: contributor
    Re-ACK d6f680b4275296e4a7f9e6ea693149e59800e495
  52. fanquake merged this on Mar 19, 2026
  53. fanquake closed this on Mar 19, 2026


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-04-10 21:13 UTC

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