diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 08:29:44 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 08:29:44 +0000 |
| commit | 2c0410b20893383ec6f1c4c8cd5134e420513e80 (patch) | |
| tree | cb0716e5770e67a429de6f9f8bdf7c0009f980c9 /src/main.cpp | |
| parent | 73e77c3bc259a4c8e0a4cc5c376f1a9545439bdd (diff) | |
net: fail over a stalled block fetch to another peer
The 15s retry spacing only helps when several peers announce the block:
the queued retries are created by inv handling, so a block announced by
a single peer that then goes quiet was re-requested from nobody, and the
tip stayed frozen until somebody happened to re-announce (#33).
Track every block getdata in mapBlockFetch, erase the entry when the
body arrives, and let SendMessages re-issue a request that has gone
unanswered for BLOCK_FETCH_RETRY_SECS to the next peer that has not had
a turn this round, restarting the round once every connected peer has.
Client peers are skipped -- they declared they cannot serve bodies.
Tracking is capped at 8 in-flight blocks so initial download, where
hundreds are legitimately outstanding to one peer, is not duplicated to
everybody else, and an entry is dropped after 10 minutes so a hash that
was announced and never existed cannot be chased forever.
The failover log line is deliberately not behind a debug category: it
firing means a peer announces blocks it does not serve, which is the
pathology being diagnosed.
Diffstat (limited to 'src/main.cpp')
| -rw-r--r-- | src/main.cpp | 105 |
1 files changed, 105 insertions, 0 deletions
diff --git a/src/main.cpp b/src/main.cpp index 7b3e555..d9fe713 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -64,6 +64,31 @@ CBlockIndex* pindexBest = NULL; | |||
| 64 | map<uint256, CBlock*> mapOrphanBlocks; | 64 | map<uint256, CBlock*> mapOrphanBlocks; |
| 65 | multimap<uint256, CBlock*> mapOrphanBlocksByPrev; | 65 | multimap<uint256, CBlock*> mapOrphanBlocksByPrev; |
| 66 | 66 | ||
| 67 | // Block bodies we have sent a getdata for and not yet received, so the | ||
| 68 | // failover pass in SendMessages can tell "requested and being served" from | ||
| 69 | // "requested and gone quiet" and re-issue the request to a different peer | ||
| 70 | // (#33). Keyed by block hash; setAsked is the peers already tried since the | ||
| 71 | // round last restarted, so the retry rotates instead of hammering the | ||
| 72 | // announcer that is not serving. Guarded by cs_main, which both the message | ||
| 73 | // handlers and SendMessages already hold. | ||
| 74 | // | ||
| 75 | // Capped small on purpose. At the tip there is one block in flight; during | ||
| 76 | // initial download there are legitimately hundreds outstanding to one peer, | ||
| 77 | // and rotating those would duplicate the whole download to every other peer. | ||
| 78 | // Anything past the cap is simply not tracked and behaves as before. | ||
| 79 | struct CBlockFetch | ||
| 80 | { | ||
| 81 | int64 nTimeFirstAsked; // when the first getdata for this block went out | ||
| 82 | int64 nTimeAsked; // when the latest getdata went out | ||
| 83 | set<CAddress> setAsked; // peers tried since the round last restarted | ||
| 84 | }; | ||
| 85 | map<uint256, CBlockFetch> mapBlockFetch; | ||
| 86 | static const int BLOCK_FETCH_TRACK_MAX = 8; | ||
| 87 | // A block nobody has served for this long is not a fetch problem: the hash may | ||
| 88 | // have been announced and never existed. Drop the entry rather than getdata | ||
| 89 | // forever; a re-announcement recreates it. | ||
| 90 | static const int BLOCK_FETCH_GIVEUP_SECS = 10 * 60; | ||
| 91 | |||
| 67 | map<uint256, CDataStream*> mapOrphanTransactions; | 92 | map<uint256, CDataStream*> mapOrphanTransactions; |
| 68 | multimap<uint256, CDataStream*> mapOrphanTransactionsByPrev; | 93 | multimap<uint256, CDataStream*> mapOrphanTransactionsByPrev; |
| 69 | 94 | ||
| @@ -3268,6 +3293,12 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) | |||
| 3268 | CInv inv(MSG_BLOCK, pblock->GetHash()); | 3293 | CInv inv(MSG_BLOCK, pblock->GetHash()); |
| 3269 | pfrom->AddInventoryKnown(inv); | 3294 | pfrom->AddInventoryKnown(inv); |
| 3270 | 3295 | ||
| 3296 | // The body is here, so the fetch is over whatever validation says | ||
| 3297 | // next -- unlike the mapAlreadyAskedFor erase below, which stays | ||
| 3298 | // conditional on acceptance so a peer feeding us garbage does not | ||
| 3299 | // cancel the scheduled retries for the real thing. | ||
| 3300 | mapBlockFetch.erase(inv.hash); | ||
| 3301 | |||
| 3271 | if (ProcessBlock(pfrom, pblock.release())) | 3302 | if (ProcessBlock(pfrom, pblock.release())) |
| 3272 | { | 3303 | { |
| 3273 | mapAlreadyAskedFor.erase(inv); | 3304 | mapAlreadyAskedFor.erase(inv); |
| @@ -3396,12 +3427,86 @@ bool SendMessages(CNode* pto) | |||
| 3396 | const CInv& inv = (*pto->mapAskFor.begin()).second; | 3427 | const CInv& inv = (*pto->mapAskFor.begin()).second; |
| 3397 | if (LogAcceptsCategory("net")) printf("sending getdata: %s\n", inv.ToString().c_str()); | 3428 | if (LogAcceptsCategory("net")) printf("sending getdata: %s\n", inv.ToString().c_str()); |
| 3398 | if (!AlreadyHave(txdb, inv)) | 3429 | if (!AlreadyHave(txdb, inv)) |
| 3430 | { | ||
| 3399 | vAskFor.push_back(inv); | 3431 | vAskFor.push_back(inv); |
| 3432 | // Blocks get their delivery tracked so the failover pass | ||
| 3433 | // below can chase the ones that go quiet. See mapBlockFetch | ||
| 3434 | // for why anything past the cap is left untracked. | ||
| 3435 | if (inv.type == MSG_BLOCK && | ||
| 3436 | (mapBlockFetch.count(inv.hash) || mapBlockFetch.size() < BLOCK_FETCH_TRACK_MAX)) | ||
| 3437 | { | ||
| 3438 | CBlockFetch& fetch = mapBlockFetch[inv.hash]; | ||
| 3439 | if (fetch.nTimeFirstAsked == 0) | ||
| 3440 | fetch.nTimeFirstAsked = GetTime(); | ||
| 3441 | fetch.nTimeAsked = GetTime(); | ||
| 3442 | fetch.setAsked.insert(pto->addr); | ||
| 3443 | } | ||
| 3444 | } | ||
| 3400 | pto->mapAskFor.erase(pto->mapAskFor.begin()); | 3445 | pto->mapAskFor.erase(pto->mapAskFor.begin()); |
| 3401 | } | 3446 | } |
| 3402 | if (!vAskFor.empty()) | 3447 | if (!vAskFor.empty()) |
| 3403 | pto->PushMessage("getdata", vAskFor); | 3448 | pto->PushMessage("getdata", vAskFor); |
| 3404 | 3449 | ||
| 3450 | |||
| 3451 | // | ||
| 3452 | // Message: getdata (block fetch failover) | ||
| 3453 | // | ||
| 3454 | // A requested block that has not arrived in BLOCK_FETCH_RETRY_SECS is | ||
| 3455 | // not still in transit -- a body is at most 1 MB and somebody | ||
| 3456 | // demonstrably has it -- so ask somebody else instead of waiting on | ||
| 3457 | // whoever was asked first (#33). Each peer the message handler visits | ||
| 3458 | // takes at most one turn per block per round; once every connected | ||
| 3459 | // peer has had its turn and a further interval passes with nobody | ||
| 3460 | // new to ask, the round restarts. A peer that declared itself a | ||
| 3461 | // client cannot serve block bodies and is no use as a target. | ||
| 3462 | if (!pto->fClient) | ||
| 3463 | { | ||
| 3464 | vector<CInv> vFailover; | ||
| 3465 | int64 nNowSecs = GetTime(); | ||
| 3466 | for (map<uint256, CBlockFetch>::iterator mi = mapBlockFetch.begin(); mi != mapBlockFetch.end();) | ||
| 3467 | { | ||
| 3468 | CBlockFetch& fetch = (*mi).second; | ||
| 3469 | CInv inv(MSG_BLOCK, (*mi).first); | ||
| 3470 | if (AlreadyHave(txdb, inv)) | ||
| 3471 | { | ||
| 3472 | // Arrived by some other road -- our own miner, an orphan | ||
| 3473 | // parked in mapOrphanBlocks. Nothing left to chase. | ||
| 3474 | mapBlockFetch.erase(mi++); | ||
| 3475 | continue; | ||
| 3476 | } | ||
| 3477 | if (nNowSecs - fetch.nTimeFirstAsked > BLOCK_FETCH_GIVEUP_SECS) | ||
| 3478 | { | ||
| 3479 | printf("block fetch: giving up on %s -- no peer served it in %d s\n", | ||
| 3480 | inv.ToString().c_str(), (int)(nNowSecs - fetch.nTimeFirstAsked)); | ||
| 3481 | mapBlockFetch.erase(mi++); | ||
| 3482 | continue; | ||
| 3483 | } | ||
| 3484 | if (nNowSecs - fetch.nTimeAsked >= BLOCK_FETCH_RETRY_SECS) | ||
| 3485 | { | ||
| 3486 | // The handler loop visits every peer within ~100ms, so a | ||
| 3487 | // request stale by two full intervals means nobody outside | ||
| 3488 | // setAsked was there to ask. Start the round over. | ||
| 3489 | if (nNowSecs - fetch.nTimeAsked >= 2 * BLOCK_FETCH_RETRY_SECS) | ||
| 3490 | fetch.setAsked.clear(); | ||
| 3491 | if (!fetch.setAsked.count(pto->addr)) | ||
| 3492 | { | ||
| 3493 | // Deliberately not behind a debug category: this line | ||
| 3494 | // firing is a peer announcing blocks it does not | ||
| 3495 | // serve, which is the pathology of #33. | ||
| 3496 | printf("block fetch failover: %s unanswered for %d s, re-requesting from %s\n", | ||
| 3497 | inv.ToString().c_str(), (int)(nNowSecs - fetch.nTimeAsked), | ||
| 3498 | pto->addr.ToString().c_str()); | ||
| 3499 | vFailover.push_back(inv); | ||
| 3500 | fetch.nTimeAsked = nNowSecs; | ||
| 3501 | fetch.setAsked.insert(pto->addr); | ||
| 3502 | } | ||
| 3503 | } | ||
| 3504 | ++mi; | ||
| 3505 | } | ||
| 3506 | if (!vFailover.empty()) | ||
| 3507 | pto->PushMessage("getdata", vFailover); | ||
| 3508 | } | ||
| 3509 | |||
| 3405 | } | 3510 | } |
| 3406 | return true; | 3511 | return true; |
| 3407 | } | 3512 | } |