diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 16:06:42 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 16:06:42 +0000 |
| commit | a215505c9e6b63883fa6058a29cc5dc7b22acd94 (patch) | |
| tree | 74648c27b62644b9020ac3f02b062dfc943b5fd1 /src/main.cpp | |
| parent | 2e20434e563b09c5209d0a1f6784d96ed8b18851 (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.cpp | 110 |
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. | ||
| 79 | struct CBlockFetch | 90 | struct 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 | }; |
| 85 | map<uint256, CBlockFetch> mapBlockFetch; | 97 | map<uint256, CBlockFetch> mapBlockFetch; |
| 86 | static const int BLOCK_FETCH_TRACK_MAX = 8; | 98 | static 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. |
| 90 | static const int BLOCK_FETCH_GIVEUP_SECS = 10 * 60; | 102 | static 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. | ||
| 124 | static 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 | |||
| 92 | map<uint256, CDataStream*> mapOrphanTransactions; | 160 | map<uint256, CDataStream*> mapOrphanTransactions; |
| 93 | multimap<uint256, CDataStream*> mapOrphanTransactionsByPrev; | 161 | multimap<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()); |