diff --git a/changelog.d/milestone-renote.md b/changelog.d/milestone-renote.md new file mode 100644 index 000000000..f4423d7eb --- /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 ff965ab21..15031c8b3 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 30f86675a..9f67dd74f 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,84 @@ 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]