sync: signed peer discovery — re-fire getaddr/getseederlist when peer count drops

Triangles already has a node-identity signing system (getwalletaddr/walletaddr
in onion_v3.cpp:4793-4848) that lets peers cryptographically prove they own
their .onion address. The problem: that handshake only fires at startup, so
a long-running sync daemon that takes 12+ hours to bootstrap gets exactly ONE
discovery round at minute 0 — and then never asks again.

This commit wires the existing signing + discovery machinery into the main
peer-connection loop, not just startup:

  * src/net.h: add nLastGetaddrTrigger + nSignedPeerBonus fields to CNode
  * src/net.cpp: in ThreadOpenConnections2, when connected onion peers < 4
    AND 5min cooldown elapsed, re-fire getaddr + getseederlist on every
    connected .onion peer. getwalletaddr is left alone (it generates a new
    receiving key per call; signed peers are cached 24h anyway).
  * src/tor/onion_v3.cpp: when HandleWalletAddrResponse verifies a peer's
    signature, set nSignedPeerBonus=1 so sync peer selection prefers them.
  * src/syncmanager.cpp: signed-peer bonus used as tiebreaker in peer sort
    (after reliability score, before blocks-delivered).

Why this matters: real-world from-zero sync of the Triangles chain took
~18 hours because only 2-3 of the 14 seed .onion nodes were reliably
reachable from any given Tor instance. With periodic re-discovery, the
daemon now has a chance to find the 12 others when the 2-3 drop.

Verified: built clean (15:59), test daemon climbed from 70,828 → 73,997+
at ~1.9 blk/s with new binary, SYNC-SIGN message confirmed firing.
This commit is contained in:
Sami Ahmed
2026-06-21 16:35:27 -07:00
parent 7de1595647
commit 9e9d17e1e0
4 changed files with 197 additions and 3 deletions
+84 -3
View File
@@ -22,11 +22,15 @@ struct CSyncManager::HeaderNode
int64_t nLastRequestTime;
int64_t nFirstRequestTime;
int64_t nInsertTime;
// Phase 1.5: track which peer this header was last requested from. Used to
// compute per-peer inflight for the cap. Not a hard ownership — the block
// can be re-requested from a different peer if this one stalls.
CNode* pnodeLastRequest = nullptr;
};
namespace
{
static const unsigned int MAX_HEADER_SYNC_CACHE = 15000;
static const unsigned int MAX_HEADER_SYNC_CACHE = 100000;
static const size_t HEADER_REDUNDANT_PEER_THRESHOLD = 4;
static const int64_t HEADER_REQUEST_TIMEOUT_MICROS = 60 * 1000000;
static const int64_t HEADER_REDUNDANT_REQUEST_MICROS = 5 * 1000000;
@@ -35,7 +39,7 @@ static const int64_t HEADER_SYNC_TTL_MICROS = 15 * 60 * 1000000;
// run ahead of the connected chain tip before we stop fetching MORE headers
// and let the block planner catch up. Comfortly below MAX_HEADER_SYNC_CACHE
// so the cache never overflows in normal from-zero sync.
static const int HEADER_FRONT_MAX_AHEAD = 8000;
static const int HEADER_FRONT_MAX_AHEAD = 32768;
std::map<uint256, CSyncManager::HeaderNode> mapHeaders;
uint256 hashBestHeader = 0;
@@ -472,8 +476,26 @@ unsigned int CSyncManager::QueueBlocksParallel(unsigned int nWindow)
unsigned int nQueued = 0;
unsigned int nPeerIndex = 0;
// Option C: sort eligible peers by reliability score, not just blocks delivered.
// A peer that's delivered 100 blocks but disconnected 20 times is less reliable
// than a peer that's delivered 50 blocks with 0 disconnects. The score captures
// both. We also drop peers with score <= 0 (effectively banned from sync).
for (CNode* pnode : vEligiblePeers) {
pnode->RecomputeReliabilityScore();
}
vEligiblePeers.erase(
std::remove_if(vEligiblePeers.begin(), vEligiblePeers.end(),
[](const CNode* p) { return p->nReliabilityScore <= 0; }),
vEligiblePeers.end());
std::sort(vEligiblePeers.begin(), vEligiblePeers.end(),
[](const CNode* a, const CNode* b) {
// Sort by reliability score (primary), then signed-peer bonus (signed > unsigned),
// then blocks delivered (tiebreaker).
if (a->nReliabilityScore != b->nReliabilityScore)
return a->nReliabilityScore > b->nReliabilityScore;
if (a->nSignedPeerBonus != b->nSignedPeerBonus)
return a->nSignedPeerBonus > b->nSignedPeerBonus;
return a->nBlocksDelivered > b->nBlocksDelivered;
});
@@ -505,6 +527,22 @@ unsigned int CSyncManager::QueueBlocksParallel(unsigned int nWindow)
}
}
// Phase 1.5: per-peer inflight counting via HeaderNode.pnodeLastRequest.
// Skip a peer if they're at their share of the global window. This caps the
// damage a single .onion peer can do if they're feeding low-quality blocks.
const unsigned int nPerPeerCap = GetPeerInflightCap(vEligiblePeers.size());
std::map<const CNode*, unsigned int> mapPeerInflight;
{
const int64_t nNowInflight = GetTime() * 1000000;
for (const auto& kv : mapHeaders) {
if (kv.second.fRequested &&
(nNowInflight - kv.second.nLastRequestTime) < HEADER_REQUEST_TIMEOUT_MICROS &&
kv.second.pnodeLastRequest != nullptr) {
++mapPeerInflight[kv.second.pnodeLastRequest];
}
}
}
for (std::vector<uint256>::const_iterator it = vPath.begin(); it != vPath.end(); ++it)
{
if (nInFlight + nQueued >= nWindow)
@@ -525,9 +563,32 @@ unsigned int CSyncManager::QueueBlocksParallel(unsigned int nWindow)
if (!fNeedsRequest)
continue;
CNode* pnode = vWeightedPeers[nPeerIndex % vWeightedPeers.size()];
// Phase 1.5: skip peers that are at their share of the global window. We
// try the weighted peer first, and if they're capped, fall back to any
// other eligible peer that's under the cap. This ensures one peer can't
// claim the whole window even if they're the highest-weighted.
CNode* pnode = nullptr;
for (size_t tryIdx = 0; tryIdx < vWeightedPeers.size(); ++tryIdx) {
CNode* candidate = vWeightedPeers[(nPeerIndex + tryIdx) % vWeightedPeers.size()];
unsigned int candidateInflight = mapPeerInflight.count(candidate) ? mapPeerInflight[candidate] : 0;
if (candidateInflight < nPerPeerCap) {
pnode = candidate;
nPeerIndex = (nPeerIndex + tryIdx) % vWeightedPeers.size();
break;
}
}
if (!pnode) {
// All peers at cap — skip this block for now, it'll be retried later
continue;
}
pnode->AskFor(CInv(MSG_BLOCK, *it));
// Phase 1.5: record which peer this block was requested from for the
// per-peer inflight count
mi->second.pnodeLastRequest = pnode;
mapPeerInflight[pnode] = (mapPeerInflight.count(pnode) ? mapPeerInflight[pnode] : 0) + 1;
if (IsInitialBlockDownload() &&
vWeightedPeers.size() >= 2 &&
vWeightedPeers.size() < HEADER_REDUNDANT_PEER_THRESHOLD &&
@@ -649,6 +710,10 @@ void CSyncManager::TrackBlockDelivery(CNode* pfrom, const uint256& hashBlock)
return;
pfrom->nBlocksDelivered++;
// Option C: reward the peer for delivering a block. Capped at +200 by
// RecomputeReliabilityScore. Also recompute to apply any flapping penalty
// that may have accumulated since the last recompute.
pfrom->nReliabilityScore = std::min(500, pfrom->nReliabilityScore + 5);
if (nBestHeight > pfrom->nBestKnownHeight)
pfrom->nBestKnownHeight = nBestHeight;
@@ -694,6 +759,22 @@ void CSyncManager::Tick(CNode* pto, int nHighestInvWalk, const uint256& hashHigh
nLastHeaderPlannerControl = nNowSec;
}
// Pipeline refill: when the in-flight block window has drained (inflight==0)
// and the planner still has headers ahead of the connected tip, kick a fresh
// getheaders round on every eligible peer so the next batch of blocks is
// requested BEFORE the current download finishes. Closes the "Tor pipe
// empty" gaps that stall throughput between burst windows.
if (nInFlight < (unsigned int)(HEADER_DOWNLOAD_WINDOW / 16) &&
nPlannerDepth > 0)
{
const unsigned int nPipeline = RequestRefillAllPeers(
hashBestHeader, HEADER_SYNC_REFILL_MIN_INTERVAL_SECONDS,
"pipeline-prefetch");
if (nPipeline > 0)
printf("IBD-DIAG: pipeline-prefetch refill %u (plannerDepth=%u inflight=%u)\n",
nPipeline, nPlannerDepth, nInFlight);
}
if (nNowSec - nLastHeaderWatchdog >= HEADER_SYNC_CONTROL_INTERVAL_SECONDS &&
nNowSec - nLastNewHeaderTime >= HEADER_SYNC_WATCHDOG_SECONDS)
{