diff --git a/CHANGELOG.md b/CHANGELOG.md index 0371346d9..babf104b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,17 @@ before 1.0). ## [Unreleased] +### Fixed + +- **`cmpct_differential` fill vs Core extra-txn:** Core v31 latches IBD in + `UpdateIBDStatus` (`LoadChainTip` / connect), not `setmocktime`. A fresh + regtest genesis stays in IBD under the default 24h `-maxtipage`, so P2P + `tx` is dropped and the fill seed panics `ours=[] core=[1]`. P2P + `bitcoind` now gets `-maxtipage=999999999`. +- **`store_reorg` overnight ASan timeout:** equal-work siblings park in + `held_bodies` and `try_apply_held` walks all of them. The fuzz hub is + reopened every 16 applies so a 4-byte unit cannot run past `-timeout=30`. + ### Changed - **Workspace version 0.6.99:** in-tree toward 0.7.0. diff --git a/TESTING.md b/TESTING.md index 4dab69102..5912f664d 100644 --- a/TESTING.md +++ b/TESTING.md @@ -277,7 +277,7 @@ in-tree extras (`store_reorg`, later `script_kernel_differential` / | `block_csv_differential` | BIP68 relative lock (full `u32` nSequence + version + MTP `time_shift`) vs Core `submitblock` | same tarball | | `mempool_differential` | `MempoolHub::test_accept` vs Core `testmempoolaccept`. **Consensus-class only** — Core standardness / fee / RBF / dust is skip (COMPAT) | same tarball, `-acceptnonstdtxn=1` | | `script_verify_differential` | `verify_tx_scripts_detached` vs Core `testmempoolaccept` of the parent+spend package. Same policy skip | same tarball, `-acceptnonstdtxn=1` | -| `store_reorg` | Tiny-hub `{extend, sibling, rewind}` connect churn (ASan, no Core). Store `Corrupt` / probe-exhausted **panics** | none | +| `store_reorg` | Tiny-hub `{extend, sibling, rewind}` connect churn (ASan, no Core). Equal-work siblings park in `held_bodies`; the hub is reopened every 16 applies so `try_apply_held` stays inside `-timeout=30`. Store `Corrupt` / probe-exhausted **panics** | none | | `script_kernel_differential` | In-process `verify_tx_scripts_detached_forks` vs `bitcoinconsensus::verify_with_flags` (ASan, **fuzz workspace only**) | Core interpreter via `bitcoinconsensus` crate | | `p2p_sequence_differential` | Up to 8 `{ping, headers, block}` steps vs live Core v2 + `compare_one` for block | same tarball, `-listen=1` | @@ -346,8 +346,11 @@ count, prefill mask, fill/duplicate/corrupt flags, nonce) then encodes a well-formed `cmpctblock`. `data[0] % 8 == 7` is the raw-wire arm (`prepare_cmpct_fuzz_hsi` restamp; malformed decode is skip). Each case grinds a unique header (prev = genesis) so Core treats it as a new compact. -Spawn `setmocktime`s Core to regtest genesis time (`CanDirectFetch` / not -IBD). Fill-flag extras are sent as `tx` first (Core extra-txn / orphan pool) +Spawn `setmocktime`s Core to regtest genesis time (`CanDirectFetch`). +P2P `bitcoind` also gets `-maxtipage=999999999` so Core v31's IBD latch +(`UpdateIBDStatus` on `LoadChainTip`, not `setmocktime`) leaves IBD at +genesis — otherwise P2P `tx` is dropped and fill extra-txn never matches. +Fill-flag extras are sent as `tx` first (Core extra-txn / orphan pool) and included in our short-id map. Missing indexes must match Core `getblocktxn`. Fully reconstructed (empty missing, Core sends no request) is a comparison. After a compared case, Core `invalidateblock`s that diff --git a/crates/rbitcoin-net/src/block_diff.rs b/crates/rbitcoin-net/src/block_diff.rs index b75b17634..b6491306b 100644 --- a/crates/rbitcoin-net/src/block_diff.rs +++ b/crates/rbitcoin-net/src/block_diff.rs @@ -252,6 +252,15 @@ pub fn store_reorg_apply(hub: &ChainHub, data: &[u8]) -> Result { Ok(n) } +/// Equal-work siblings are parked in `held_bodies`; `try_apply_held` walks +/// all of them. Overnight ASan `-timeout=30` fires once the persistent hub +/// has accumulated too many. Recycle the fuzz hub this often. +pub const STORE_REORG_RECYCLE_EVERY: u64 = 16; + +pub fn store_reorg_recycle_hub(apply_n: u64) -> bool { + apply_n > 0 && apply_n.is_multiple_of(STORE_REORG_RECYCLE_EVERY) +} + pub fn mine_diff_pad(hub: &ChainHub, last: u32) -> Result { if last < 1 { return Err("pad last"); @@ -2150,6 +2159,15 @@ mod tests { assert!(verdict_from_accept(Err(NetError::Io(std::io::Error::other("x")))).is_err()); } + #[test] + fn store_reorg_recycle_hub_every_sixteen_applies() { + assert!(!store_reorg_recycle_hub(0)); + assert!(!store_reorg_recycle_hub(15)); + assert!(store_reorg_recycle_hub(STORE_REORG_RECYCLE_EVERY)); + assert!(store_reorg_recycle_hub(STORE_REORG_RECYCLE_EVERY * 2)); + assert!(!store_reorg_recycle_hub(STORE_REORG_RECYCLE_EVERY + 1)); + } + #[test] fn store_reorg_three_ops_do_not_corrupt() { let (dir, hub, _tip) = tmp_diff_hub(); diff --git a/crates/rbitcoin-net/src/lib.rs b/crates/rbitcoin-net/src/lib.rs index 8d9dd6bd6..415339735 100644 --- a/crates/rbitcoin-net/src/lib.rs +++ b/crates/rbitcoin-net/src/lib.rs @@ -33,10 +33,10 @@ pub use block_diff::{ mine_diff_stem, parse_submitblock_json, parse_testmempoolaccept_json, prepare_csv_age_candidate, prepare_height1_candidate, prepare_script_candidate, prepare_spend_candidate, rewind_oracle_until, setup_side_block, split_http_body, - store_reorg_apply, store_reorg_corrupt_is_finding, store_reorg_step, submit_pad_to_oracle, - submit_side_to_oracle, verdict_from_accept, verdict_from_core_reply, wait_for_file, - BlockOracle, CompareOne, DiffPad, DiffTip, DiffVerdict, OracleReply, StoreReorgOp, - BLOCK_STRUCT_CTRL, DIFF_MATURE_PAD_HEIGHT, DIFF_REORG_N, DIFF_TEST_PAD_HEIGHT, + store_reorg_apply, store_reorg_corrupt_is_finding, store_reorg_recycle_hub, store_reorg_step, + submit_pad_to_oracle, submit_side_to_oracle, verdict_from_accept, verdict_from_core_reply, + wait_for_file, BlockOracle, CompareOne, DiffPad, DiffTip, DiffVerdict, OracleReply, + StoreReorgOp, BLOCK_STRUCT_CTRL, DIFF_MATURE_PAD_HEIGHT, DIFF_REORG_N, DIFF_TEST_PAD_HEIGHT, }; pub use cache::BlockCache; pub use chain::{ diff --git a/fuzz/fuzz_targets/store_reorg.rs b/fuzz/fuzz_targets/store_reorg.rs index 44dcf2a81..4b022149d 100644 --- a/fuzz/fuzz_targets/store_reorg.rs +++ b/fuzz/fuzz_targets/store_reorg.rs @@ -2,12 +2,14 @@ use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::OnceLock; +use std::sync::Mutex; use libfuzzer_sys::fuzz_target; use rbitcoin_consensus::Milestone; use rbitcoin_fuzz::tmp_dir; -use rbitcoin_net::{check_diff_env, diff_regtest_params, store_reorg_apply, ChainHub}; +use rbitcoin_net::{ + check_diff_env, diff_regtest_params, store_reorg_apply, store_reorg_recycle_hub, ChainHub, +}; use rbitcoin_query::Query; struct Base { @@ -15,7 +17,8 @@ struct Base { _store: PathBuf, } -static BASE: OnceLock = OnceLock::new(); +static STATE: Mutex> = Mutex::new(None); +static APPLIES: AtomicU64 = AtomicU64::new(0); static COMPARISONS: AtomicU64 = AtomicU64::new(0); fn harness_failure(what: &str) -> ! { @@ -31,34 +34,37 @@ fn note_comparison(k: u32) { } } -fn base() -> &'static Base { - BASE.get_or_init(|| { - if std::env::var_os("RBITCOIN_HEAD_SCALE").is_none() { - std::env::set_var("RBITCOIN_HEAD_SCALE", "tiny"); - } - if std::env::var_os("RBITCOIN_IO").is_none() { - std::env::set_var("RBITCOIN_IO", "fd"); - } - let head = std::env::var("RBITCOIN_HEAD_SCALE").ok(); - let io = std::env::var("RBITCOIN_IO").ok(); - if let Err(e) = check_diff_env(head.as_deref(), io.as_deref()) { - harness_failure(e); - } - let store = tmp_dir("rbtc-store-reorg"); - let q = Query::open_or_create(store.join("store")).unwrap_or_else(|e| { - harness_failure(&format!("query open: {e}")); - }); - let hub = ChainHub::new(q, diff_regtest_params(), Milestone::NONE); - hub.ensure_genesis() - .unwrap_or_else(|e| harness_failure(&format!("genesis: {e}"))); - Base { hub, _store: store } - }) +fn open_base() -> Base { + if std::env::var_os("RBITCOIN_HEAD_SCALE").is_none() { + std::env::set_var("RBITCOIN_HEAD_SCALE", "tiny"); + } + if std::env::var_os("RBITCOIN_IO").is_none() { + std::env::set_var("RBITCOIN_IO", "fd"); + } + let head = std::env::var("RBITCOIN_HEAD_SCALE").ok(); + let io = std::env::var("RBITCOIN_IO").ok(); + if let Err(e) = check_diff_env(head.as_deref(), io.as_deref()) { + harness_failure(e); + } + let store = tmp_dir("rbtc-store-reorg"); + let q = Query::open_or_create(store.join("store")).unwrap_or_else(|e| { + harness_failure(&format!("query open: {e}")); + }); + let hub = ChainHub::new(q, diff_regtest_params(), Milestone::NONE); + hub.ensure_genesis() + .unwrap_or_else(|e| harness_failure(&format!("genesis: {e}"))); + Base { hub, _store: store } } fuzz_target!(|data: &[u8]| { - let b = base(); + let n = APPLIES.fetch_add(1, Ordering::Relaxed); + let mut slot = STATE.lock().unwrap_or_else(|e| e.into_inner()); + if slot.is_none() || store_reorg_recycle_hub(n) { + *slot = Some(open_base()); + } + let b = slot.as_ref().unwrap(); match store_reorg_apply(&b.hub, data) { - Ok(n) if n > 0 => note_comparison(n), + Ok(k) if k > 0 => note_comparison(k), Ok(_) => {} Err(msg) => panic!("store_reorg: {msg}"), } diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 52eca0507..e521bcad5 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -207,6 +207,10 @@ impl Drop for CoreChild { } /// Argv for a listening BIP324 peer. RPC-only [`spawn_bitcoind`] stays `-listen=0`. +/// +/// `-maxtipage=999999999`: Core v31 latches IBD in `UpdateIBDStatus` (startup +/// `LoadChainTip` / connect), not `setmocktime`. Default 24h keeps a 2011 +/// regtest genesis in IBD, so P2P `tx` is dropped (cmpct fill extra-txn miss). pub fn bitcoind_p2p_args(datadir: &Path, rpcport: u16, p2pport: u16, cookie: &Path) -> Vec { vec![ "-regtest".into(), @@ -220,6 +224,7 @@ pub fn bitcoind_p2p_args(datadir: &Path, rpcport: u16, p2pport: u16, cookie: &Pa "-dnsseed=0".into(), "-listenonion=0".into(), "-printtoconsole=0".into(), + "-maxtipage=999999999".into(), format!("-datadir={}", datadir.display()), "-rpcbind=127.0.0.1".into(), "-rpcallowip=127.0.0.1".into(), @@ -389,6 +394,10 @@ mod tests { assert!(args.iter().any(|a| a == "-dnsseed=0")); assert!(args.iter().any(|a| a == "-listenonion=0")); assert!(args.iter().any(|a| a == "-port=18444")); + assert!( + args.iter().any(|a| a == "-maxtipage=999999999"), + "P2P bitcoind must leave IBD at genesis so fill txs reach extra-txn" + ); } #[test]