diff options
| author | Bitflash <btf01@proton.me> | 2026-07-31 14:26:50 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-31 14:26:50 -0300 |
| commit | e2d5c1bbe01370dbf75e2118d697a159c4ee582b (patch) | |
| tree | f109392ac356c46bdb248c554110813c8f7ad19c /src | |
| parent | 3268ea95c1443238d380dea26e7b1bb3bd108aed (diff) | |
net: bound orphan blocks and stop asking a peer the same question twice (#70)
Two defects that feed each other. Measured on a 32-core production node:
1287 of 4117 processed blocks arrived with a missing parent, 97 arrived
that the node already had, and 490 transactions arrived whose inputs were
unknown.
mapOrphanBlocks had no ceiling at all. Orphan transactions have had one
since 0.1.0 -- MAX_ORPHAN_TRANSACTIONS with LimitOrphanTx -- and blocks
never did. An orphan block whose parent never turns up stays in memory for
the life of the process, each one holding a whole block, and a peer can
send an unlimited number of them just by inventing the previous-block hash.
LimitOrphanBlocks mirrors the transaction one: random eviction, both maps
kept in step, and the CBlock deleted because mapOrphanBlocks owns it.
Every orphan block also fired a getblocks at the sender, and an inv for a
block already held as an orphan fired another, with no filtering. So a node
missing part of the chain asked the same peer the same question over and
over, and the duplicate answers made more orphans, which asked again.
CNode::PushGetBlocks remembers the last request and drops a repeat -- the
same fix Bitcoin made in e2c2648c1.
One trap found by testing rather than by reading: eviction picks at random
and may pick the block just inserted, so GetOrphanRoot has to be read out
of pblock before LimitOrphanBlocks runs, not after. The first version had
that backwards and would have read freed memory.
Verified with fabricated orphans, four of the seven sharing one
hashPrevBlock so the multimap path is exercised: 7 in, limit(3) leaves 3,
limit(0) leaves 0, both maps in step at every stage, no crash. Then a run
against the live network to 4348 with no scaffolding left behind.
The storm itself was not reproducible on demand -- the network is healthy
today and produced one orphan block in seven minutes -- so there is no
before/after on the metric that motivated this.
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.cpp | 50 | ||||
| -rw-r--r-- | src/main.h | 9 | ||||
| -rw-r--r-- | src/net.cpp | 11 | ||||
| -rw-r--r-- | src/net.h | 26 |
4 files changed, 93 insertions, 3 deletions
diff --git a/src/main.cpp b/src/main.cpp index b147ddd..4bc9c4d 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -293,6 +293,43 @@ void AddOrphanTx(const CDataStream& vMsg) | |||
| 293 | // grow this map until the node runs out of memory. Evict at random rather than | 293 | // grow this map until the node runs out of memory. Evict at random rather than |
| 294 | // in map order, so an attacker cannot keep its own orphans resident by | 294 | // in map order, so an attacker cannot keep its own orphans resident by |
| 295 | // choosing hashes that sort low. | 295 | // choosing hashes that sort low. |
| 296 | // Drop orphan blocks at random until we are back under the ceiling. | ||
| 297 | // | ||
| 298 | // Random rather than oldest-first, matching LimitOrphanTx below: there is no | ||
| 299 | // arrival order recorded, and an attacker who knew the eviction rule could aim | ||
| 300 | // at it. Dropping one costs nothing that matters -- if its parent ever shows | ||
| 301 | // up, the block is requested again like any other. | ||
| 302 | // | ||
| 303 | // Both maps have to be kept in step, and the CBlock itself has to be deleted: | ||
| 304 | // mapOrphanBlocks owns it. | ||
| 305 | void LimitOrphanBlocks(unsigned int nMaxOrphans) | ||
| 306 | { | ||
| 307 | while (mapOrphanBlocks.size() > nMaxOrphans) | ||
| 308 | { | ||
| 309 | uint256 randomhash; | ||
| 310 | for (int i = 0; i < 4; i++) | ||
| 311 | ((uint64*)&randomhash)[i] = GetRand(_UI64_MAX); | ||
| 312 | map<uint256, CBlock*>::iterator it = mapOrphanBlocks.lower_bound(randomhash); | ||
| 313 | if (it == mapOrphanBlocks.end()) | ||
| 314 | it = mapOrphanBlocks.begin(); | ||
| 315 | |||
| 316 | CBlock* pblock = it->second; | ||
| 317 | uint256 hashPrev = pblock->hashPrevBlock; | ||
| 318 | |||
| 319 | for (multimap<uint256, CBlock*>::iterator mi = mapOrphanBlocksByPrev.lower_bound(hashPrev); | ||
| 320 | mi != mapOrphanBlocksByPrev.upper_bound(hashPrev);) | ||
| 321 | { | ||
| 322 | if (mi->second == pblock) | ||
| 323 | mapOrphanBlocksByPrev.erase(mi++); | ||
| 324 | else | ||
| 325 | ++mi; | ||
| 326 | } | ||
| 327 | |||
| 328 | mapOrphanBlocks.erase(it); | ||
| 329 | delete pblock; | ||
| 330 | } | ||
| 331 | } | ||
| 332 | |||
| 296 | void LimitOrphanTx(unsigned int nMaxOrphans) | 333 | void LimitOrphanTx(unsigned int nMaxOrphans) |
| 297 | { | 334 | { |
| 298 | while (mapOrphanTransactions.size() > nMaxOrphans) | 335 | while (mapOrphanTransactions.size() > nMaxOrphans) |
| @@ -1468,9 +1505,16 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock) | |||
| 1468 | mapOrphanBlocks.insert(make_pair(hash, pblock)); | 1505 | mapOrphanBlocks.insert(make_pair(hash, pblock)); |
| 1469 | mapOrphanBlocksByPrev.insert(make_pair(pblock->hashPrevBlock, pblock)); | 1506 | mapOrphanBlocksByPrev.insert(make_pair(pblock->hashPrevBlock, pblock)); |
| 1470 | 1507 | ||
| 1508 | // Read what we need out of pblock before trimming: eviction picks at | ||
| 1509 | // random and is perfectly entitled to pick the block just inserted, | ||
| 1510 | // which would leave the GetOrphanRoot call below reading freed memory. | ||
| 1511 | uint256 hashOrphanRoot = GetOrphanRoot(pblock); | ||
| 1512 | |||
| 1513 | LimitOrphanBlocks(MAX_ORPHAN_BLOCKS); | ||
| 1514 | |||
| 1471 | // Ask this guy to fill in what we're missing | 1515 | // Ask this guy to fill in what we're missing |
| 1472 | if (pfrom) | 1516 | if (pfrom) |
| 1473 | pfrom->PushMessage("getblocks", CBlockLocator(pindexBest), GetOrphanRoot(pblock)); | 1517 | pfrom->PushGetBlocks(pindexBest, hashOrphanRoot); |
| 1474 | return true; | 1518 | return true; |
| 1475 | } | 1519 | } |
| 1476 | 1520 | ||
| @@ -2047,7 +2091,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) | |||
| 2047 | if (!fAskedForBlocks && !pfrom->fClient) | 2091 | if (!fAskedForBlocks && !pfrom->fClient) |
| 2048 | { | 2092 | { |
| 2049 | fAskedForBlocks = true; | 2093 | fAskedForBlocks = true; |
| 2050 | pfrom->PushMessage("getblocks", CBlockLocator(pindexBest), uint256(0)); | 2094 | pfrom->PushGetBlocks(pindexBest, uint256(0)); |
| 2051 | } | 2095 | } |
| 2052 | 2096 | ||
| 2053 | // Introduce ourselves and hand over the .btf peers that answered us. | 2097 | // Introduce ourselves and hand over the .btf peers that answered us. |
| @@ -2110,7 +2154,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) | |||
| 2110 | if (!fAlreadyHave) | 2154 | if (!fAlreadyHave) |
| 2111 | pfrom->AskFor(inv); | 2155 | pfrom->AskFor(inv); |
| 2112 | else if (inv.type == MSG_BLOCK && mapOrphanBlocks.count(inv.hash)) | 2156 | else if (inv.type == MSG_BLOCK && mapOrphanBlocks.count(inv.hash)) |
| 2113 | pfrom->PushMessage("getblocks", CBlockLocator(pindexBest), GetOrphanRoot(mapOrphanBlocks[inv.hash])); | 2157 | pfrom->PushGetBlocks(pindexBest, GetOrphanRoot(mapOrphanBlocks[inv.hash])); |
| 2114 | } | 2158 | } |
| 2115 | } | 2159 | } |
| 2116 | 2160 | ||
| @@ -18,6 +18,13 @@ static const unsigned int MAX_SIZE = 0x02000000; | |||
| 18 | // Ceiling on transactions held waiting for a parent that has not arrived. | 18 | // Ceiling on transactions held waiting for a parent that has not arrived. |
| 19 | // They cost a peer nothing to send and are never validated, only stored. | 19 | // They cost a peer nothing to send and are never validated, only stored. |
| 20 | static const unsigned int MAX_ORPHAN_TRANSACTIONS = 100; | 20 | static const unsigned int MAX_ORPHAN_TRANSACTIONS = 100; |
| 21 | // Blocks held waiting for a parent that has not arrived. Orphan transactions | ||
| 22 | // have had a ceiling since 0.1.0 and orphan blocks never did: one whose parent | ||
| 23 | // never turns up stays in memory for the life of the process, and a peer can | ||
| 24 | // send an unlimited number of them by inventing the previous-block hash. They | ||
| 25 | // are not free either -- each holds a whole block. A production node was | ||
| 26 | // seeing 1287 of them against 4117 blocks processed. | ||
| 27 | static const unsigned int MAX_ORPHAN_BLOCKS = 200; | ||
| 21 | // Signature operations allowed in one block. Size alone does not bound what a | 28 | // Signature operations allowed in one block. Size alone does not bound what a |
| 22 | // block costs to validate: every OP_CHECKSIG is an elliptic-curve verification, | 29 | // block costs to validate: every OP_CHECKSIG is an elliptic-curve verification, |
| 23 | // so a block within the size limit could still hold millions of them and take | 30 | // so a block within the size limit could still hold millions of them and take |
| @@ -125,6 +132,8 @@ void PrintBlockTree(); | |||
| 125 | void AddOrphanTx(const CDataStream& vMsg); | 132 | void AddOrphanTx(const CDataStream& vMsg); |
| 126 | void EraseOrphanTx(uint256 hash); | 133 | void EraseOrphanTx(uint256 hash); |
| 127 | void LimitOrphanTx(unsigned int nMaxOrphans); | 134 | void LimitOrphanTx(unsigned int nMaxOrphans); |
| 135 | // Same ceiling idea for orphan blocks, which never had one. | ||
| 136 | void LimitOrphanBlocks(unsigned int nMaxOrphans); | ||
| 128 | // nThreadId is 1..N, only for logging: it makes each miner's lines | 137 | // nThreadId is 1..N, only for logging: it makes each miner's lines |
| 129 | // distinguishable, which the dedup filter would otherwise collapse into one. | 138 | // distinguishable, which the dedup filter would otherwise collapse into one. |
| 130 | bool BitcoinMiner(int nThreadId = 1); | 139 | bool BitcoinMiner(int nThreadId = 1); |
diff --git a/src/net.cpp b/src/net.cpp index ac8df82..37c081f 100644 --- a/src/net.cpp +++ b/src/net.cpp | |||
| @@ -181,6 +181,17 @@ bool AnySubscribed(unsigned int nChannel) | |||
| 181 | return false; | 181 | return false; |
| 182 | } | 182 | } |
| 183 | 183 | ||
| 184 | void CNode::PushGetBlocks(CBlockIndex* pindexBegin, uint256 hashEnd) | ||
| 185 | { | ||
| 186 | // Filter out duplicate requests. | ||
| 187 | if (pindexBegin == pindexLastGetBlocksBegin && hashEnd == hashLastGetBlocksEnd) | ||
| 188 | return; | ||
| 189 | pindexLastGetBlocksBegin = pindexBegin; | ||
| 190 | hashLastGetBlocksEnd = hashEnd; | ||
| 191 | |||
| 192 | PushMessage("getblocks", CBlockLocator(pindexBegin), hashEnd); | ||
| 193 | } | ||
| 194 | |||
| 184 | bool CNode::IsSubscribed(unsigned int nChannel) | 195 | bool CNode::IsSubscribed(unsigned int nChannel) |
| 185 | { | 196 | { |
| 186 | if (nChannel >= vfSubscribe.size()) | 197 | if (nChannel >= vfSubscribe.size()) |
| @@ -7,6 +7,9 @@ class CAddress; | |||
| 7 | class CInv; | 7 | class CInv; |
| 8 | class CRequestTracker; | 8 | class CRequestTracker; |
| 9 | class CNode; | 9 | class CNode; |
| 10 | // Defined in main.h, which is included after this file. Only ever used here as | ||
| 11 | // a pointer, so the forward declaration is enough. | ||
| 12 | class CBlockIndex; | ||
| 10 | 13 | ||
| 11 | 14 | ||
| 12 | 15 | ||
| @@ -533,6 +536,11 @@ public: | |||
| 533 | // Last "btfpeers" we accepted from this node, to rate-limit the exchange. | 536 | // Last "btfpeers" we accepted from this node, to rate-limit the exchange. |
| 534 | int64 nLastPexRecv; | 537 | int64 nLastPexRecv; |
| 535 | 538 | ||
| 539 | // Last getblocks we sent this peer, so we do not ask the same question | ||
| 540 | // over and over. See CNode::PushGetBlocks. | ||
| 541 | CBlockIndex* pindexLastGetBlocksBegin; | ||
| 542 | uint256 hashLastGetBlocksEnd; | ||
| 543 | |||
| 536 | // Liveness. A peer whose network path dies silently -- NAT drops an idle | 544 | // Liveness. A peer whose network path dies silently -- NAT drops an idle |
| 537 | // mapping, a relay restarts, a route changes -- never sends FIN, so the | 545 | // mapping, a relay restarts, a route changes -- never sends FIN, so the |
| 538 | // socket stays readable-never and the node simply stops hearing from it | 546 | // socket stays readable-never and the node simply stops hearing from it |
| @@ -565,6 +573,8 @@ public: | |||
| 565 | nRefCount = 0; | 573 | nRefCount = 0; |
| 566 | nReleaseTime = 0; | 574 | nReleaseTime = 0; |
| 567 | nLastPexRecv = 0; | 575 | nLastPexRecv = 0; |
| 576 | pindexLastGetBlocksBegin = NULL; | ||
| 577 | hashLastGetBlocksEnd = 0; | ||
| 568 | nTimeConnected = GetTime(); | 578 | nTimeConnected = GetTime(); |
| 569 | nLastSend = 0; | 579 | nLastSend = 0; |
| 570 | nLastRecv = 0; | 580 | nLastRecv = 0; |
| @@ -717,6 +727,22 @@ public: | |||
| 717 | 727 | ||
| 718 | 728 | ||
| 719 | 729 | ||
| 730 | // Ask this peer to fill in blocks we are missing, but only once per | ||
| 731 | // question. | ||
| 732 | // | ||
| 733 | // Every orphan block fires one of these, and an inv for a block we already | ||
| 734 | // hold as an orphan fires another. A node that is missing part of the | ||
| 735 | // chain therefore asks the same peer the same thing repeatedly: measured | ||
| 736 | // on a production node, 1287 orphan blocks out of 4117 processed, and 97 | ||
| 737 | // "already have block" from the answers coming back more than once. The | ||
| 738 | // duplicate answers create more orphans, which fire more requests. | ||
| 739 | // | ||
| 740 | // Remembering the last question asked breaks that loop. Same fix Bitcoin | ||
| 741 | // made in e2c2648c1. | ||
| 742 | // Body lives in net.cpp: it builds a CBlockLocator, which main.h defines, | ||
| 743 | // and main.h is included after this file. | ||
| 744 | void PushGetBlocks(CBlockIndex* pindexBegin, uint256 hashEnd); | ||
| 745 | |||
| 720 | void PushMessage(const char* pszCommand) | 746 | void PushMessage(const char* pszCommand) |
| 721 | { | 747 | { |
| 722 | try | 748 | try |