From f5c6d36b50b80a09eb836c1f66726154e5984cc6 Mon Sep 17 00:00:00 2001 From: "rearden-grok[bot]" <317016512+rearden-grok[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:06:59 -0700 Subject: [PATCH 1/2] test: resume seed must note stored headers on the milestone path A restart restores the anchor hash and then queues the stored headers. Those heights stay off the milestone map, so script checks stay on. --- crates/rbitcoin-net/src/ibd/path.rs | 80 +++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/crates/rbitcoin-net/src/ibd/path.rs b/crates/rbitcoin-net/src/ibd/path.rs index 30f86675a1..b092a944d9 100644 --- a/crates/rbitcoin-net/src/ibd/path.rs +++ b/crates/rbitcoin-net/src/ibd/path.rs @@ -157,6 +157,9 @@ fn install_resume( t0.elapsed() ); plant_valid_tip_child(st, hub); + // The anchor was restored before these rows were queued. Note the linked + // prefix so script checks turn back off. + super::header_walk::renote_stored_path(st, hub); } /// If tip+1 is missing or invalid, plant a known valid sibling/child of tip. @@ -576,6 +579,83 @@ mod tests { let _ = std::fs::remove_dir_all(dir); } + /// Stored headers ahead of the tip are the milestone path. A restart + /// that already restored the anchor must note them so script checks stay + /// off while those blocks confirm. + #[test] + fn resume_seed_notes_stored_headers_on_the_milestone_path() { + use super::seed_work_path_from_store; + use bitcoin::block::{Header, Version}; + use bitcoin::{BlockHash, CompactTarget}; + use rbitcoin_consensus::{Milestone, MilestoneAnchor}; + + let (dir, mut hub) = crate::chain::tiny_regtest_hub_labeled("path-seed-milestone"); + hub.ensure_genesis().unwrap(); + let anchor = BlockHash::from_byte_array([0xab; 32]); + hub.milestone = Milestone { + height: 100, + anchor: Some(MilestoneAnchor { + hash: anchor, + min_work_be: [0u8; 32], + }), + }; + hub.query.note_milestone_header( + 100, + anchor.to_byte_array(), + [0u8; 32], + bitcoin::Work::from_be_bytes([0u8; 32]), + None, + ); + + let mut prev = hub.tip_hash().unwrap(); + let mut first = None; + for n in 1u32..=2 { + let mut header = Header { + version: Version::from_consensus(4), + prev_blockhash: prev, + merkle_root: bitcoin::TxMerkleNode::from_byte_array([n as u8; 32]), + time: 1_600_000_000 + n, + bits: CompactTarget::from_consensus(0x207f_ffff), + nonce: n, + }; + rbitcoin_consensus::grind_regtest_pow(&mut header); + hub.ensure_header(&header).unwrap(); + if first.is_none() { + first = Some(header.block_hash()); + } + prev = header.block_hash(); + } + let first = first.unwrap(); + assert!( + hub.query.milestone_header_at(1).is_none(), + "the anchor note alone does not record the stored child" + ); + + let mut st = IbdWorkState::new(Vec::new(), hub.tip_hash(), hub.tip_height()); + seed_work_path_from_store(&mut st, &hub); + assert!( + st.hash_height.contains_key(&first), + "resume seed queued the stored header" + ); + assert_eq!( + hub.query.milestone_header_at(1), + Some(first.to_byte_array()), + "a seeded header on the stored path is on the milestone path" + ); + assert!( + hub.milestone.skips_scripts(1, &first.to_byte_array(), |h| hub + .query + .milestone_header_at(h)), + "script checks stay off for a stored header under the restored anchor" + ); + assert_eq!( + hub.query.milestone_header_at(100), + Some(anchor.to_byte_array()), + "noting the queue keeps the restored anchor" + ); + let _ = std::fs::remove_dir_all(dir); + } + /// Archive headers ahead of tip so resume seed walks a non-empty path /// (bodies + a body gap for the contiguous-prefix break). #[test] From eb98753e759138c5a9cdcd859f993f8f22aef24f Mon Sep 17 00:00:00 2001 From: "rearden-grok[bot]" <317016512+rearden-grok[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:10:18 -0700 Subject: [PATCH 2/2] ibd: note resumed headers on the milestone path Restart restored the anchor and then queued stored headers without noting them, so script checks stayed on for that span. The resume seed now runs the stored-path note after those rows are queued. Heights that link from the confirmed tip skip scripts again. --- changelog.d/milestone-renote.md | 3 +++ crates/rbitcoin-net/src/ibd/header_walk.rs | 7 ++++++- crates/rbitcoin-net/src/ibd/path.rs | 7 ++++--- 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 changelog.d/milestone-renote.md diff --git a/changelog.d/milestone-renote.md b/changelog.d/milestone-renote.md new file mode 100644 index 0000000000..f4423d7eb3 --- /dev/null +++ b/changelog.d/milestone-renote.md @@ -0,0 +1,3 @@ +Fixed + +- **IBD restart keeps script checks off on the stored header path.** After the anchor is restored, headers the resume seed queues from the store are noted on the milestone path again, so blocks under that anchor are not script-checked a second time. diff --git a/crates/rbitcoin-net/src/ibd/header_walk.rs b/crates/rbitcoin-net/src/ibd/header_walk.rs index ff965ab217..15031c8b3a 100644 --- a/crates/rbitcoin-net/src/ibd/header_walk.rs +++ b/crates/rbitcoin-net/src/ibd/header_walk.rs @@ -3055,7 +3055,12 @@ pub(crate) fn note_off_path( seen.len() > OFF_PATH_HEADER_BUDGET as usize } -fn renote_stored_path(st: &IbdWorkState, hub: &ChainHub) { +/// Note stored work-path headers on the milestone map. +/// +/// Restart restores the anchor before the resume seed fills this map, so +/// the seed calls this again. A height that links from the confirmed tip is +/// on the path that reached the anchor. +pub(crate) fn renote_stored_path(st: &IbdWorkState, hub: &ChainHub) { let mut height = hub.tip_height().unwrap_or(0); let mut prev = hub.tip_hash(); while let Some(next_h) = height.checked_add(1) { diff --git a/crates/rbitcoin-net/src/ibd/path.rs b/crates/rbitcoin-net/src/ibd/path.rs index b092a944d9..9f67dd74fe 100644 --- a/crates/rbitcoin-net/src/ibd/path.rs +++ b/crates/rbitcoin-net/src/ibd/path.rs @@ -643,9 +643,10 @@ mod tests { "a seeded header on the stored path is on the milestone path" ); assert!( - hub.milestone.skips_scripts(1, &first.to_byte_array(), |h| hub - .query - .milestone_header_at(h)), + hub.milestone + .skips_scripts(1, &first.to_byte_array(), |h| hub + .query + .milestone_header_at(h)), "script checks stay off for a stored header under the restored anchor" ); assert_eq!(