From f3539c16c094aaf0b712d48f69392c802e6dc0dc Mon Sep 17 00:00:00 2001 From: frstrtr Date: Tue, 16 Jun 2026 11:14:01 +0000 Subject: [PATCH] btc(v36): add think-independent 10s re-advert timer leg (root-2) Mirror the LTC timer leg from #104/e01c1606. The event leg (readvertise_best on think work-refresh + clean_tracker best-change) goes dead exactly when think() wedges -- the livelock-adjacent scenario this fix family targets. Arm a one-shot core::Timer(10s) -> readvertise_best() in the run_think IO-phase, fired exactly once when the verified chain first becomes non-empty, so a peer that handshook during the empty window can still ingest our chain even if a later think() cycle wedges. Composes with the merged #97 think-watchdog: timer runs on the IO thread, pure reads, broadcast stays try_to_lock, no new mutex. --- src/impl/btc/node.cpp | 15 +++++++++++++++ src/impl/btc/node.hpp | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/src/impl/btc/node.cpp b/src/impl/btc/node.cpp index cfd0aecfd..56ea7e5bc 100644 --- a/src/impl/btc/node.cpp +++ b/src/impl/btc/node.cpp @@ -1624,6 +1624,21 @@ void NodeImpl::run_think() << " verified_heads=" << m_tracker.verified.get_heads().size(); } + // ROOT-2: fire ONE think-independent delayed re-advert when the + // verified chain first becomes non-empty. Covers peers that + // handshook during the empty window and so never see a best-change + // event. The one-shot timer runs on the IO thread, so it still + // fires even if a later think() cycle wedges (composes with the + // #97 think-watchdog). Pure reads; broadcast stays try_to_lock. + if (m_verified_was_empty && m_tracker.verified.size() > 0) { + m_verified_was_empty = false; + if (!m_readvert_timer) + m_readvert_timer = std::make_unique(m_context, false); + m_readvert_timer->start(10, [this]() { readvertise_best(); }); + LOG_INFO << "[readvertise] verified chain populated — scheduled " + "10s re-advert (ROOT-2)"; + } + // Drain share batches queued while think() held the mutex LOG_INFO << "[ASYNC-THINK] IO-phase: draining " << m_pending_adds.size() << " pending batches, peers=" << m_peers.size(); diff --git a/src/impl/btc/node.hpp b/src/impl/btc/node.hpp index 8c87260c2..82954402d 100644 --- a/src/impl/btc/node.hpp +++ b/src/impl/btc/node.hpp @@ -517,6 +517,7 @@ class NodeImpl : public pool::BaseNode size_t m_max_peers = 30; size_t m_target_outbound_peers = DEFAULT_TARGET_OUTBOUND_PEERS; std::unique_ptr m_connect_timer; + std::unique_ptr m_readvert_timer; // one-shot ROOT-2 re-advert std::set m_pending_outbound; // addresses currently being dialed std::set m_outbound_addrs; // successfully connected outbound peers @@ -552,6 +553,10 @@ class NodeImpl : public pool::BaseNode // Cached best share hash from the most recent think() cycle uint256 m_best_share_hash; + // ROOT-2: fire exactly one delayed re-advert when the verified chain + // first transitions from empty to non-empty (peers that handshook + // during the empty window otherwise never see a best-change event). + bool m_verified_was_empty = true; // Cache of original raw serialized bytes keyed by share hash. // Used for relay so we send the exact bytes we received, avoiding