diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 16:00:59 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 16:00:59 +0000 |
| commit | 2e20434e563b09c5209d0a1f6784d96ed8b18851 (patch) | |
| tree | a20d2e40b6382063145cea8de0801a1747afedd3 /src/main.cpp | |
| parent | 2c0410b20893383ec6f1c4c8cd5134e420513e80 (diff) | |
net: keep a block tracked until its body actually validates
The mapBlockFetch entry was erased before ProcessBlock ran, on the
reasoning that the body had arrived so the fetch was over either way.
That reasoning does not survive the fact that GetHash() covers the
80-byte header and nothing else.
Any peer that saw X announced holds X's header. It can answer our
getdata with that header and a transaction list of its own invention;
the reply still hashes to X, so the entry was erased, and CheckBlock
then rejected the body on hashMerkleRoot and stored nothing. AlreadyHave
stayed false, the mapAskFor entry had already been consumed when the
getdata went out, and nothing recreated the tracking -- so X was never
requested from anybody again unless some peer happened to re-announce
it. That is the #33 stall, reachable on demand for one message.
The erase now sits inside the success branch, beside the
mapAlreadyAskedFor erase it belongs with. ProcessBlock returns true in
exactly two cases, block accepted and block parked as an orphan, and
both store it where AlreadyHave will find it, so one erase covers both
and neither needs different treatment. Of the false cases, the two
already-have ones are cleared by the sweep's own AlreadyHave check on
its next pass, and the rest are rejections that must keep the entry.
A mismatched merkle root is also now grounds to drop the peer. Nothing
honest produces it: peer traffic rides the .btf channel, which is
XChaCha20-Poly1305, so a body that arrives at all arrived intact, and
the mismatch is the peer's own construction. Checking it in the handler
also rejects a forged body before CheckBlock spends a RandomX hash on
it. The unconditional log line fires once per connection, because
ProcessMessages keeps draining the receive buffer after fDisconnect is
set and a peer that packs many forged bodies into one buffer must not
get a line for each.
Diffstat (limited to 'src/main.cpp')
| -rw-r--r-- | src/main.cpp | 50 |
1 files changed, 45 insertions, 5 deletions
diff --git a/src/main.cpp b/src/main.cpp index d9fe713..8926d57 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -3293,16 +3293,56 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) | |||
| 3293 | CInv inv(MSG_BLOCK, pblock->GetHash()); | 3293 | CInv inv(MSG_BLOCK, pblock->GetHash()); |
| 3294 | pfrom->AddInventoryKnown(inv); | 3294 | pfrom->AddInventoryKnown(inv); |
| 3295 | 3295 | ||
| 3296 | // The body is here, so the fetch is over whatever validation says | 3296 | // GetHash() covers the 80-byte header and nothing else, so the hash |
| 3297 | // next -- unlike the mapAlreadyAskedFor erase below, which stays | 3297 | // coming out right does not make this the block we asked for. Every |
| 3298 | // conditional on acceptance so a peer feeding us garbage does not | 3298 | // peer that saw X announced holds X's header, and can answer our |
| 3299 | // cancel the scheduled retries for the real thing. | 3299 | // getdata with that header bolted onto a transaction list of its own |
| 3300 | mapBlockFetch.erase(inv.hash); | 3300 | // invention -- the reply still hashes to X. hashMerkleRoot is the only |
| 3301 | // thing binding the header to the body, so check it here, before the | ||
| 3302 | // mapBlockFetch entry is touched and before CheckBlock spends a | ||
| 3303 | // RandomX hash on a body already known to be forged. | ||
| 3304 | // | ||
| 3305 | // There is no honest way to arrive here. Peer traffic rides the .btf | ||
| 3306 | // channel, which is XChaCha20-Poly1305, so a body that arrives at all | ||
| 3307 | // arrived intact: a mismatch is the peer's own construction, not line | ||
| 3308 | // noise. Drop the peer and leave the fetch outstanding so the failover | ||
| 3309 | // sweep asks somebody else for the real X, rather than recording X as | ||
| 3310 | // delivered on the strength of a header anyone could copy. | ||
| 3311 | if (pblock->hashMerkleRoot != pblock->BuildMerkleTree()) | ||
| 3312 | { | ||
| 3313 | if (LogAcceptsCategory("net")) | ||
| 3314 | printf("peer %s served a forged body for %s\n", | ||
| 3315 | pfrom->addr.ToString().c_str(), inv.ToString().c_str()); | ||
| 3316 | // One line per connection: ProcessMessages keeps draining the | ||
| 3317 | // receive buffer after fDisconnect is set, so a peer that packs | ||
| 3318 | // many forged bodies into one buffer must not get one log line | ||
| 3319 | // for each of them. | ||
| 3320 | if (!pfrom->fDisconnect) | ||
| 3321 | printf("block fetch: a peer answered the request for %s with a body that is not " | ||
| 3322 | "that block -- dropping it\n", inv.hash.ToString().substr(0,14).c_str()); | ||
| 3323 | pfrom->fDisconnect = true; | ||
| 3324 | return error("ProcessMessage() : forged block body for %s", | ||
| 3325 | inv.hash.ToString().substr(0,14).c_str()); | ||
| 3326 | } | ||
| 3301 | 3327 | ||
| 3302 | if (ProcessBlock(pfrom, pblock.release())) | 3328 | if (ProcessBlock(pfrom, pblock.release())) |
| 3303 | { | 3329 | { |
| 3304 | mapAlreadyAskedFor.erase(inv); | 3330 | mapAlreadyAskedFor.erase(inv); |
| 3305 | 3331 | ||
| 3332 | // The body arrived and validated, so the fetch really is over: | ||
| 3333 | // the block is now in mapBlockIndex, or parked in mapOrphanBlocks, | ||
| 3334 | // and either way AlreadyHave answers for it from here on. | ||
| 3335 | // | ||
| 3336 | // This has to stay inside the success branch, next to the erase | ||
| 3337 | // above. When validation rejects a body nothing is stored, | ||
| 3338 | // AlreadyHave stays false, and the mapAskFor entry was already | ||
| 3339 | // consumed when the getdata went out -- so erasing the tracking | ||
| 3340 | // here would leave the block requested from nobody at all until | ||
| 3341 | // some peer happened to re-announce it. That is exactly the stall | ||
| 3342 | // of #33, and erasing before ProcessBlock made it reachable on | ||
| 3343 | // demand. | ||
| 3344 | mapBlockFetch.erase(inv.hash); | ||
| 3345 | |||
| 3306 | // A peer that just handed us a block is demonstrably at least at | 3346 | // A peer that just handed us a block is demonstrably at least at |
| 3307 | // that block's height. Without this, nStartingHeight is a snapshot | 3347 | // that block's height. Without this, nStartingHeight is a snapshot |
| 3308 | // taken once during the version handshake and never touched again, | 3348 | // taken once during the version handshake and never touched again, |