Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 93 additions & 1 deletion src/impl/ltc/node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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<std::shared_mutex> lock(m_tracker_mutex, std::try_to_lock);
if (!lock.owns_lock())
return;

if (!m_chain->contains(head))
return;

std::vector<uint256> 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.
Expand Down Expand Up @@ -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<core::Timer>(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();
Expand Down
21 changes: 21 additions & 0 deletions src/impl/ltc/node.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,21 @@ class NodeImpl : public pool::BaseNode<ltc::Config, ltc::ShareChain, ltc::Peer>
/// 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();
Expand Down Expand Up @@ -487,6 +502,7 @@ class NodeImpl : public pool::BaseNode<ltc::Config, ltc::ShareChain, ltc::Peer>
size_t m_max_peers = 30;
size_t m_target_outbound_peers = DEFAULT_TARGET_OUTBOUND_PEERS;
std::unique_ptr<core::Timer> m_connect_timer;
std::unique_ptr<core::Timer> m_readvert_timer; // one-shot ROOT-2 re-advert
std::set<NetService> m_pending_outbound; // addresses currently being dialed
std::set<NetService> m_outbound_addrs; // successfully connected outbound peers

Expand Down Expand Up @@ -522,6 +538,11 @@ class NodeImpl : public pool::BaseNode<ltc::Config, ltc::ShareChain, ltc::Peer>
// 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.
Expand Down
Loading