Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 6 additions & 3 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions crates/rbitcoin-net/src/block_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,15 @@ pub fn store_reorg_apply(hub: &ChainHub, data: &[u8]) -> Result<u32, String> {
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<DiffPad, &'static str> {
if last < 1 {
return Err("pad last");
Expand Down Expand Up @@ -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();
Expand Down
8 changes: 4 additions & 4 deletions crates/rbitcoin-net/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down
60 changes: 33 additions & 27 deletions fuzz/fuzz_targets/store_reorg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@

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 {
hub: ChainHub,
_store: PathBuf,
}

static BASE: OnceLock<Base> = OnceLock::new();
static STATE: Mutex<Option<Base>> = Mutex::new(None);
static APPLIES: AtomicU64 = AtomicU64::new(0);
static COMPARISONS: AtomicU64 = AtomicU64::new(0);

fn harness_failure(what: &str) -> ! {
Expand All @@ -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}"),
}
Expand Down
9 changes: 9 additions & 0 deletions fuzz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
vec![
"-regtest".into(),
Expand All @@ -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(),
Expand Down Expand Up @@ -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]
Expand Down