upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/main.cpp
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-02 16:06:42 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-02 16:06:42 +0000
commita215505c9e6b63883fa6058a29cc5dc7b22acd94 (patch)
tree74648c27b62644b9020ac3f02b062dfc943b5fd1 /src/main.cpp
parent2e20434e563b09c5209d0a1f6784d96ed8b18851 (diff)
net: make room in the block fetch table instead of refusing new blocks
Nothing on the path from inv to insertion can tell a real block hash from thirty-two random bytes. AlreadyHave only asks whether we already hold the block, and with no header there is no work to check. So one inv carrying eight hashes that never existed filled all eight slots, and because a full table refused new entries outright, no genuinely new block was tracked again until the give-up timer expired ten minutes later. One message per ten minutes from a single connection switched failover off for the whole node -- not eight wasted slots, an off-switch. A full table now evicts. Eviction on its own would just be the same attack in a new shape, so the victim is chosen by occupancy: a peer may only take a slot from a peer holding strictly more slots than itself, and otherwise recycles one of its own oldest. A peer that has filled the table is the heaviest holder by definition, so its own entries are the only ones it can displace; a peer announcing its first block holds nothing, so it always evicts from the heaviest. A genuine announcement is therefore never refused a slot, whatever an attacker holds or across however many connections it holds it. Entries carry the announcing peer for this, set once on creation -- the failover sweep asks other peers later and none of them chose to announce the hash, so none of them should be charged for it. The two timestamps also get member initialisers. The insertion path uses operator[] and tests nTimeFirstAsked == 0 to spot a fresh entry, which worked on value-initialisation alone, but only until somebody gave the struct a constructor -- and this commit gives it a member that wants one.
Diffstat (limited to 'src/main.cpp')
-rw-r--r--src/main.cpp110
1 files changed, 97 insertions, 13 deletions
diff --git a/src/main.cpp b/src/main.cpp
index 8926d57..9ab1f44 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -75,12 +75,24 @@ multimap<uint256, CBlock*> mapOrphanBlocksByPrev;
75// Capped small on purpose. At the tip there is one block in flight; during 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, 76// initial download there are legitimately hundreds outstanding to one peer,
77// and rotating those would duplicate the whole download to every other 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. 78//
79// The cap is a shared resource and nothing on the path from inv to insertion
80// can tell a real block hash from 32 random bytes -- AlreadyHave only asks
81// whether we already hold the block, and there is no header to check work
82// against. So the table does not refuse a new hash when it is full; it makes
83// room. See MakeRoomInBlockFetch for the rule, which is what stops one peer
84// filling all eight slots and switching failover off for everybody.
85//
86// The initialisers matter: the insertion path uses operator[] and then tests
87// nTimeFirstAsked == 0 to detect a fresh entry. That worked on
88// value-initialisation alone, but only for as long as nobody gave this struct
89// a constructor.
79struct CBlockFetch 90struct CBlockFetch
80{ 91{
81 int64 nTimeFirstAsked; // when the first getdata for this block went out 92 int64 nTimeFirstAsked = 0; // when the first getdata for this block went out
82 int64 nTimeAsked; // when the latest getdata went out 93 int64 nTimeAsked = 0; // when the latest getdata went out
83 set<CAddress> setAsked; // peers tried since the round last restarted 94 CAddress addrAnnounced; // peer whose announcement put this hash here
95 set<CAddress> setAsked; // peers tried since the round last restarted
84}; 96};
85map<uint256, CBlockFetch> mapBlockFetch; 97map<uint256, CBlockFetch> mapBlockFetch;
86static const int BLOCK_FETCH_TRACK_MAX = 8; 98static const int BLOCK_FETCH_TRACK_MAX = 8;
@@ -89,6 +101,62 @@ static const int BLOCK_FETCH_TRACK_MAX = 8;
89// forever; a re-announcement recreates it. 101// forever; a re-announcement recreates it.
90static const int BLOCK_FETCH_GIVEUP_SECS = 10 * 60; 102static const int BLOCK_FETCH_GIVEUP_SECS = 10 * 60;
91 103
104// Drop one entry so a hash announced by addrNew can be tracked.
105//
106// Refusing the insertion instead was a remote off-switch for the whole
107// feature. An inv carrying eight hashes that never existed filled every slot
108// -- nothing between inv and here can tell those from real blocks -- and no
109// genuinely new block was tracked again until the give-up timer expired ten
110// minutes later, re-armable for one message per ten minutes from a single
111// connection.
112//
113// So evict instead, by occupancy: a peer may only take a slot from a peer
114// holding strictly more slots than itself, and otherwise recycles one of its
115// own. That is what keeps eviction from becoming the same attack in a new
116// shape. A peer that has filled the table is by definition the heaviest
117// holder, so its own entries are the only ones it can displace, while a peer
118// announcing its first block holds nothing and therefore always evicts from
119// the heaviest -- a genuine announcement is never refused a slot, whatever the
120// attacker holds or however many connections it holds it from. The one case
121// that costs a real fetch is eight distinct peers holding one entry each,
122// where a ninth takes the oldest; that is a busy healthy node, not an attack,
123// and losing the oldest of eight beats refusing the ninth.
124static void MakeRoomInBlockFetch(const CAddress& addrNew)
125{
126 if (mapBlockFetch.empty())
127 return;
128
129 map<CAddress, int> mapHeld;
130 for (map<uint256, CBlockFetch>::iterator mi = mapBlockFetch.begin(); mi != mapBlockFetch.end(); ++mi)
131 mapHeld[(*mi).second.addrAnnounced]++;
132
133 int nMine = mapHeld.count(addrNew) ? mapHeld[addrNew] : 0;
134
135 int nMax = 0;
136 CAddress addrVictim;
137 for (map<CAddress, int>::iterator mi = mapHeld.begin(); mi != mapHeld.end(); ++mi)
138 if ((*mi).second > nMax)
139 {
140 nMax = (*mi).second;
141 addrVictim = (*mi).first;
142 }
143
144 // Nobody is holding more than this peer, so it recycles its own oldest
145 // rather than taking a slot off an equal.
146 if (nMax <= nMine)
147 addrVictim = addrNew;
148
149 map<uint256, CBlockFetch>::iterator miVictim = mapBlockFetch.end();
150 for (map<uint256, CBlockFetch>::iterator mi = mapBlockFetch.begin(); mi != mapBlockFetch.end(); ++mi)
151 if ((*mi).second.addrAnnounced == addrVictim &&
152 (miVictim == mapBlockFetch.end() ||
153 (*mi).second.nTimeFirstAsked < (*miVictim).second.nTimeFirstAsked))
154 miVictim = mi;
155
156 if (miVictim != mapBlockFetch.end())
157 mapBlockFetch.erase(miVictim);
158}
159
92map<uint256, CDataStream*> mapOrphanTransactions; 160map<uint256, CDataStream*> mapOrphanTransactions;
93multimap<uint256, CDataStream*> mapOrphanTransactionsByPrev; 161multimap<uint256, CDataStream*> mapOrphanTransactionsByPrev;
94 162
@@ -3470,16 +3538,32 @@ bool SendMessages(CNode* pto)
3470 { 3538 {
3471 vAskFor.push_back(inv); 3539 vAskFor.push_back(inv);
3472 // Blocks get their delivery tracked so the failover pass 3540 // Blocks get their delivery tracked so the failover pass
3473 // below can chase the ones that go quiet. See mapBlockFetch 3541 // below can chase the ones that go quiet.
3474 // for why anything past the cap is left untracked. 3542 if (inv.type == MSG_BLOCK)
3475 if (inv.type == MSG_BLOCK &&
3476 (mapBlockFetch.count(inv.hash) || mapBlockFetch.size() < BLOCK_FETCH_TRACK_MAX))
3477 { 3543 {
3478 CBlockFetch& fetch = mapBlockFetch[inv.hash]; 3544 // Full, and this is a hash we are not already tracking:
3479 if (fetch.nTimeFirstAsked == 0) 3545 // make room rather than drop it on the floor, or one peer
3480 fetch.nTimeFirstAsked = GetTime(); 3546 // announcing eight hashes turns failover off for everyone.
3481 fetch.nTimeAsked = GetTime(); 3547 if (!mapBlockFetch.count(inv.hash) &&
3482 fetch.setAsked.insert(pto->addr); 3548 mapBlockFetch.size() >= BLOCK_FETCH_TRACK_MAX)
3549 MakeRoomInBlockFetch(pto->addr);
3550
3551 if (mapBlockFetch.count(inv.hash) ||
3552 mapBlockFetch.size() < BLOCK_FETCH_TRACK_MAX)
3553 {
3554 CBlockFetch& fetch = mapBlockFetch[inv.hash];
3555 if (fetch.nTimeFirstAsked == 0)
3556 {
3557 fetch.nTimeFirstAsked = GetTime();
3558 // Who is answerable for this hash existing. Set
3559 // once, on creation: the failover sweep asks other
3560 // peers later, and none of them chose to announce
3561 // it, so none of them should be charged for it.
3562 fetch.addrAnnounced = pto->addr;
3563 }
3564 fetch.nTimeAsked = GetTime();
3565 fetch.setAsked.insert(pto->addr);
3566 }
3483 } 3567 }
3484 } 3568 }
3485 pto->mapAskFor.erase(pto->mapAskFor.begin()); 3569 pto->mapAskFor.erase(pto->mapAskFor.begin());