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:00:59 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-02 16:00:59 +0000
commit2e20434e563b09c5209d0a1f6784d96ed8b18851 (patch)
treea20d2e40b6382063145cea8de0801a1747afedd3 /src/main.cpp
parent2c0410b20893383ec6f1c4c8cd5134e420513e80 (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.cpp50
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,