diff --git a/src/impl/ltc/node.cpp b/src/impl/ltc/node.cpp index 74d17786f..34565dbde 100644 --- a/src/impl/ltc/node.cpp +++ b/src/impl/ltc/node.cpp @@ -254,7 +254,7 @@ void NodeImpl::send_version(peer_ptr peer) m_nonce, m_software_version, 1, // mode (always 1 for legacy compat) - best_share_hash() // advertise our tallest chain head + advertised_best_share() // verified head, or raw head pre-sync (ROOT-2) ); peer->write(std::move(rmsg)); } @@ -886,6 +886,81 @@ uint256 NodeImpl::best_share_hash() return best; } +uint256 NodeImpl::advertised_best_share() +{ + // Peer-facing advertisement ONLY (version handshake + timer re-announce). + // NEVER used for work/share creation — best_share_hash() owns that and + // deliberately returns a VERIFIED head (or NULL) so we never build local + // work on an unagreed chain. For ADVERTISEMENT the opposite is right: a + // peer must learn our tallest head to call download_shares() and pull our + // chain. ROOT-2 (Option-A net-id soak): a --genesis node whose verified + // chain is still empty at handshake advertises NULL via best_share_hash(); + // the peer never downloads and broadcast can't backfill (head shares are + // already de-dup-marked). Advertising the raw head breaks that deadlock + // without touching work creation. + uint256 v = best_share_hash(); + if (!v.IsNull()) + return v; + + // Verified chain still empty — advertise our tallest RAW head instead. + if (m_chain && m_chain->size() > 0) { + uint256 best; + int32_t best_height = -1; + for (const auto& [head_hash, tail_hash] : m_chain->get_heads()) { + auto h = m_chain->get_height(head_hash); + if (h > best_height) { + best = head_hash; + best_height = h; + } + } + return best; + } + return uint256::ZERO; +} + +void NodeImpl::readvertise_best_share() +{ + // ROOT-2 re-advertisement. A peer that finished its version handshake + // while our verified chain was empty got a NULL best_share and never + // called download_shares(); broadcast_share() can't wake it either because + // our head shares are already in m_shared_share_hashes (de-dup-marked at + // creation) so its walk breaks immediately. Here we re-push the tip walk + // to every peer WITHOUT consulting the de-dup set. Advertise-only: never + // affects local work creation. + uint256 head = advertised_best_share(); + if (head.IsNull()) + return; + + // Same try_to_lock discipline as broadcast_share (node.hpp:67): never block + // the IO thread on the tracker mutex. If think() holds it now, the next + // trigger (best-change or the timer) retries. + std::shared_lock lock(m_tracker_mutex, std::try_to_lock); + if (!lock.owns_lock()) + return; + + if (!m_chain->contains(head)) + return; + + std::vector to_send; + int32_t height = m_chain->get_height(head); + int32_t walk = std::min(height, 5); + for (auto [hash, data] : m_chain->get_chain(head, walk)) { + if (m_rejected_share_hashes.count(hash)) + continue; // never re-broadcast peer-rejected shares + to_send.push_back(hash); + } + if (to_send.empty()) + return; + + auto now = std::chrono::steady_clock::now(); + for (auto& [nonce, peer] : m_peers) { + send_shares(peer, to_send); + m_last_broadcast_to[peer->addr()] = {to_send, now}; + } + LOG_INFO << "[readvertise] re-pushed " << to_send.size() + << " head share(s) to " << m_peers.size() << " peer(s) (ROOT-2)"; +} + void NodeImpl::download_shares(peer_ptr /*unused_peer*/, const uint256& target_hash) { // p2pool node.py:108-141 download_shares() — exact translation. @@ -1477,6 +1552,23 @@ void NodeImpl::run_think() << " verified_heads=" << m_tracker.verified.get_heads().size(); } + // ROOT-2: re-advertise our head so a peer that handshook while our + // verified chain was empty can finally download it. On best-change + // push immediately; additionally fire ONE delayed re-advert when the + // verified chain first becomes non-empty (covers peers that connected + // during the empty window and never see a best-change event). + if (best_changed && !m_peers.empty()) { + readvertise_best_share(); + } + 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_share(); }); + 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/ltc/node.hpp b/src/impl/ltc/node.hpp index a2228ddb8..928efbf53 100644 --- a/src/impl/ltc/node.hpp +++ b/src/impl/ltc/node.hpp @@ -315,6 +315,21 @@ class NodeImpl : public pool::BaseNode /// Return the hash of our tallest chain head, or uint256::ZERO if empty. uint256 best_share_hash(); + /// Peer-facing advertisement of our head (version handshake + timer + /// re-announce ONLY — never work creation). Returns the verified head + /// when we have one, otherwise the tallest RAW head so a fresh peer can + /// begin download_shares() before our verified chain exists. ROOT-2: on a + /// --genesis node whose verified chain is still empty at handshake, + /// best_share_hash() advertises NULL and the peer never downloads; + /// advertising the raw head breaks that deadlock. + uint256 advertised_best_share(); + + /// Re-push our advertised head to every connected peer, bypassing the + /// broadcast_share() de-dup set. Fired on best-change and once on a timer + /// after the verified chain first becomes non-empty, so a peer that + /// handshook during the empty window can still ingest our chain. + void readvertise_best_share(); + /// Load persisted shares from LevelDB storage into the tracker. void load_persisted_shares(); void flush_verified_to_leveldb(); @@ -487,6 +502,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 @@ -522,6 +538,11 @@ 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 // any round-trip serialization differences.