upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-07-31 14:26:50 -0300
committerGitHub <noreply@github.com>2026-07-31 14:26:50 -0300
commite2d5c1bbe01370dbf75e2118d697a159c4ee582b (patch)
treef109392ac356c46bdb248c554110813c8f7ad19c /src
parent3268ea95c1443238d380dea26e7b1bb3bd108aed (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.cpp50
-rw-r--r--src/main.h9
-rw-r--r--src/net.cpp11
-rw-r--r--src/net.h26
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.
305void 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
296void LimitOrphanTx(unsigned int nMaxOrphans) 333void 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
diff --git a/src/main.h b/src/main.h
index 2d5cb86..3bc1219 100644
--- a/src/main.h
+++ b/src/main.h
@@ -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.
20static const unsigned int MAX_ORPHAN_TRANSACTIONS = 100; 20static 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.
27static 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();
125void AddOrphanTx(const CDataStream& vMsg); 132void AddOrphanTx(const CDataStream& vMsg);
126void EraseOrphanTx(uint256 hash); 133void EraseOrphanTx(uint256 hash);
127void LimitOrphanTx(unsigned int nMaxOrphans); 134void LimitOrphanTx(unsigned int nMaxOrphans);
135// Same ceiling idea for orphan blocks, which never had one.
136void 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.
130bool BitcoinMiner(int nThreadId = 1); 139bool 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
184void 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
184bool CNode::IsSubscribed(unsigned int nChannel) 195bool CNode::IsSubscribed(unsigned int nChannel)
185{ 196{
186 if (nChannel >= vfSubscribe.size()) 197 if (nChannel >= vfSubscribe.size())
diff --git a/src/net.h b/src/net.h
index 831a880..5e828c3 100644
--- a/src/net.h
+++ b/src/net.h
@@ -7,6 +7,9 @@ class CAddress;
7class CInv; 7class CInv;
8class CRequestTracker; 8class CRequestTracker;
9class CNode; 9class 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.
12class 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