Skip to content

Commit 363878e

Browse files
giaki3003claude
andcommitted
fix: P2P: block merkle roots not validated before inserting into archive DB
Bug: s2-r22 (primary) Finding: https://giaki3003.tech/#/findings/20260603-2200-thunder-peer-body-poison-archive-and-net-task-halt Severity: Medium Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 91d6457 commit 363878e

3 files changed

Lines changed: 76 additions & 5 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"sessionID": "ses_0d31354b3ffe3FPJ6zqmrkH2ni",
3+
"updatedAt": "2026-07-04T11:40:36.457Z",
4+
"sources": {
5+
"background-task": {
6+
"state": "idle",
7+
"updatedAt": "2026-07-04T11:40:36.457Z"
8+
}
9+
}
10+
}

lib/archive.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,36 @@ impl Archive {
702702
})
703703
}
704704

705+
/// Delete a stored body, reversing the effects of [`Self::put_body`].
706+
///
707+
/// Used to discard a body that was stored before its contents could be
708+
/// validated (e.g. a body received from a peer) and later turned out to be
709+
/// invalid, so that the block is reported missing again by
710+
/// [`Self::iter_missing_bodies`] and the real body is re-requested.
711+
pub fn delete_body(
712+
&self,
713+
rwtxn: &mut RwTxn,
714+
block_hash: BlockHash,
715+
body: &Body,
716+
) -> Result<(), Error> {
717+
self.bodies
718+
.delete(rwtxn, &block_hash)
719+
.map_err(DbError::from)?;
720+
body.transactions.iter().try_for_each(|tx| {
721+
let txid = tx.txid();
722+
let mut inclusions = self.get_tx_inclusions(rwtxn, txid)?;
723+
inclusions.remove(&block_hash);
724+
if inclusions.is_empty() {
725+
self.txid_to_inclusions
726+
.delete(rwtxn, &txid)
727+
.map_err(DbError::from)?;
728+
} else {
729+
self.txid_to_inclusions.put(rwtxn, &txid, &inclusions)?;
730+
}
731+
Ok(())
732+
})
733+
}
734+
705735
/// Store a header.
706736
///
707737
/// The following predicates MUST be met before calling this function:

lib/node/net_task.rs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -390,15 +390,26 @@ fn reorg_to_tip(
390390
}
391391
two_way_peg_data
392392
};
393-
let () = connect_tip_(
393+
if let Err(err) = connect_tip_(
394394
&mut rwtxn,
395395
archive,
396396
mempool,
397397
state,
398398
header,
399399
body,
400400
&two_way_peg_data,
401-
)?;
401+
) {
402+
// The stored body for this block failed validation (e.g. a peer
403+
// supplied a body whose contents do not match the header's merkle
404+
// root). Abort the reorg and discard the invalid body from the
405+
// archive so that the block is reported missing again and the real
406+
// body is re-requested, instead of the archive staying poisoned.
407+
drop(rwtxn);
408+
let mut rwtxn = env.write_txn().map_err(EnvError::from)?;
409+
let () = archive.delete_body(&mut rwtxn, header.hash(), &body)?;
410+
rwtxn.commit().map_err(RwTxnError::from)?;
411+
return Err(err);
412+
}
402413
let new_tip_hash = state.try_get_tip(&rwtxn)?.unwrap();
403414
let bmm_verification =
404415
archive.get_best_main_verification(&rwtxn, new_tip_hash)?;
@@ -1022,8 +1033,8 @@ impl NetTask {
10221033
}
10231034
}
10241035
}
1025-
MailboxItem::NewTipReady(new_tip, _addr, resp_tx) => {
1026-
let reorg_applied = task::block_in_place(|| {
1036+
MailboxItem::NewTipReady(new_tip, addr, resp_tx) => {
1037+
let reorg_result = task::block_in_place(|| {
10271038
reorg_to_tip(
10281039
&self.ctxt.env,
10291040
&self.ctxt.archive,
@@ -1033,7 +1044,27 @@ impl NetTask {
10331044
&self.ctxt.zmq_pub_handler,
10341045
new_tip,
10351046
)
1036-
})?;
1047+
});
1048+
// A tip supplied by a peer may carry a body that fails
1049+
// validation. Do not let that error propagate out of `run`
1050+
// and halt the whole net task: log it and drop the
1051+
// offending peer instead. `reorg_to_tip` has already
1052+
// discarded the invalid body so it will be re-requested.
1053+
let reorg_applied = match reorg_result {
1054+
Ok(reorg_applied) => reorg_applied,
1055+
Err(err) => {
1056+
tracing::warn!(
1057+
?addr,
1058+
%err,
1059+
"Failed to reorg to new tip; dropping peer"
1060+
);
1061+
if let Some(addr) = addr {
1062+
let () =
1063+
self.ctxt.net.remove_active_peer(addr);
1064+
}
1065+
false
1066+
}
1067+
};
10371068
if let Some(resp_tx) = resp_tx {
10381069
let () = resp_tx
10391070
.send(reorg_applied)

0 commit comments

Comments
 (0)