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
5 changes: 3 additions & 2 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ reads it; rustup users export it). Override coverage dir:
| Remining 100-block maturity pads with `confirm_wire_run` | `pad_empty_from` / `build_mature_regtest_with_spend` **once per binary journey** (not once per skinny test) |
| Wall-time multi-round microbenches in default suite | Deterministic structure / chunk-load asserts; demote wall arms to `#[ignore]` |

**Tier A timeouts:** `two_node_header_and_block_sync` 60s wall (default + job). `p2p_timeout_getaddr_and_keepalive_ping`, `p2p_hb_compact_tip_follow`, and `p2p_orphan_child_getdatas_parent` 20s wall (default). Reconstruct / dead-peer are **multinode job only** (`#[ignore]`; job passes `--ignored`). `coverage.sh` also `--skip`s those names plus `two_node`. Heavier topology stays `#[ignore]` (`scripts/integration.sh`).
**Tier A timeouts:** `two_node_header_and_block_sync` 60s wall (default + job). `p2p_timeout_getaddr_and_keepalive_ping`, `p2p_hb_compact_tip_follow`, `p2p_orphan_child_getdatas_parent`, and `p2p_compact_getblocktxn_missing_extra_tx` 20s wall (default). Reconstruct / dead-peer are **multinode job only** (`#[ignore]`; job passes `--ignored`). `coverage.sh` also `--skip`s those names plus `two_node`. Heavier topology stays `#[ignore]` (`scripts/integration.sh`).

**Speed / reliability (default suite):** prefer `pad_empty_from` / `build_mature_regtest_with_spend` **once per journey** (tx_relay live hub, Electrum protocol, core_analogs assumevalid+mempool) over remine pads; SH run-builder sleeps are 1 ms under `cfg(test)` (40 ms in production). `pin_compose_multi_pack_timed` keeps functional + layout/covered short-circuit gates (multi-ms floor); sticky vs cold assemble is log-only (not a hard timing assert). Schema-13 wire rebuild must stamp create identity from `txid.body` — zero batch identity is treated as missing (regression covered by `reconstruct_and_connect_error_arms` + multi-vout confirm scenarios). Coverage vs speed: prefer **one** scenario at the real entry over N micro-opens that only paint lines; when adding coverage for reduce/materialize, use a **tiny** target, not production stream depth.

Expand Down Expand Up @@ -238,6 +238,7 @@ Prefer **one high-level scenario** per behavior cluster. Delete lower-level test
| `two_node_header_and_block_sync` | P2P (**default + multinode CI**) | Seeder → peer 8-block IBD; peer `last_write` meter. **Not** re-run under `coverage.sh`. |
| `p2p_timeout_getaddr_and_keepalive_ping` | P2P (**default**) | One pad: v1-magic inbound drops at `peertimeout=1`, AddrFetch `getaddr`/`addrv2` (no `getheaders`), one keepalive ping/pong |
| `p2p_hb_compact_tip_follow` | P2P (**default**) | Genesis follow: first tip via headers/inv, then HB `sendcmpct` + `cmpctblock` reconstruct (coinbase-only, no `getblocktxn`) |
| `p2p_compact_getblocktxn_missing_extra_tx` | P2P (**default**) | Follow session: 2-tx compact (coinbase prefilled, extra not in mempool) → `getblocktxn`. Does **not** pin depth-10 full-block serve |
| `p2p_orphan_child_getdatas_parent` | P2P (**default**) | Follow session: missing-parent child parks; peer `GetData`s the parent. Does **not** pin log-not-reject or tokio-worker lock |
| `badprev_orphan_does_not_blacklist_then_reorg_reconstructs` | P2P/chain (default) | Orphan whose prev is not on the tip is held (not `BLOCK_FAILED`); winner branch reconstructs |
| `serve_after_restart_via_reconstruct` | P2P (**multinode job only**) | Cold serve via reconstruct |
Expand All @@ -256,7 +257,7 @@ Removed (covered by the rows above): `confirm_cross_block_prevout_without_tx_hea

### Integration / multi-node

Default `cargo test` runs `two_node_header_and_block_sync` (8-block), `p2p_timeout_getaddr_and_keepalive_ping`, `p2p_hb_compact_tip_follow`, `p2p_orphan_child_getdatas_parent`, and `badprev_orphan_does_not_blacklist_then_reorg_reconstructs`. The required **multinode** job also runs reconstruct and slim dead-peer (`--ignored` filters in `ci.yml`).
Default `cargo test` runs `two_node_header_and_block_sync` (8-block), `p2p_timeout_getaddr_and_keepalive_ping`, `p2p_hb_compact_tip_follow`, `p2p_orphan_child_getdatas_parent`, `p2p_compact_getblocktxn_missing_extra_tx`, and `badprev_orphan_does_not_blacklist_then_reorg_reconstructs`. The required **multinode** job also runs reconstruct and slim dead-peer (`--ignored` filters in `ci.yml`).
Heavy topology (3-hop, 48-block, mesh, `run_p2p`) stays `#[ignore]` for `scripts/integration.sh`:

```bash
Expand Down
98 changes: 96 additions & 2 deletions crates/rbitcoin-test/tests/integration_multinode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
//! **Tier A (default + CI `multinode` job):** single-hop IBD (8 blocks), cold
//! reconstruct serve (10 blocks). Hard wall timeouts; hang-free on CI-class hosts.
//! **Tier B (default suite):** handshake timeout / GetAddr / keepalive ping,
//! HB compact tip-follow, mempool orphan child GetData of parent, hub reorg
//! (including leftover/BadPrev orphan that must not blacklist).
//! HB compact tip-follow, compact `getblocktxn` for a missing extra tx,
//! mempool orphan child GetData of parent, hub reorg (including leftover/BadPrev
//! orphan that must not blacklist).
//! **Tier C (`#[ignore]`):** multi-hop, tip-follow, 48-block dual seeder, mesh —
//! `scripts/integration.sh` or `-- --ignored` only.

Expand Down Expand Up @@ -459,6 +460,99 @@ async fn p2p_orphan_child_getdatas_parent() {
.expect("p2p_orphan_child_getdatas_parent wall timeout (20s)");
}

/// Compact of a 2-tx tip: coinbase prefilled, extra tx absent from mempool → getblocktxn.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn p2p_compact_getblocktxn_missing_extra_tx() {
use bitcoin::bip152::HeaderAndShortIds;
use bitcoin::p2p::message::NetworkMessage;
use bitcoin::p2p::message_compact_blocks::CmpctBlock;
use bitcoin::Amount;
use rbitcoin_test::mine::spend_anyone_can_spend;

let fut = async {
let seed_dir = TempDir::new().unwrap();
let peer_dir = TempDir::new().unwrap();
let seed = start_node(&seed_dir).await;
let mut peer = start_node(&peer_dir).await;
attach_relay_mempool(&peer, &peer_dir);
tokio::time::timeout(Duration::from_secs(5), peer.follow_from(seed.local_addr))
.await
.expect("follow_from handshake")
.expect("follow");
assert!(
peer.follow_live_count() >= 1,
"outbound follow must stay live"
);

let extra = spend_anyone_can_spend(
bitcoin::Txid::from_byte_array([0x33; 32]),
0,
Amount::from_sat(1000),
);
let tip = seed.hub.tip_hash().expect("genesis");
let tip_time = seed.hub.tip_header().expect("genesis header").time;
let block = mine_regtest_block(tip, tip_time + 600, 1, vec![extra]);
assert_eq!(block.txdata.len(), 2, "coinbase + extra");
let hsi = HeaderAndShortIds::from_block(&block, 1, 2, &[0]).expect("compact hsi");
assert_eq!(
hsi.short_ids.len(),
1,
"coinbase prefilled; extra is a short-id"
);

let writer_deadline = tokio::time::Instant::now() + Duration::from_secs(3);
loop {
let queued = seed.peers.live_peers().into_iter().any(|p| {
p.inbound
&& p.handshake_complete()
&& p.queue_msg(NetworkMessage::CmpctBlock(CmpctBlock {
compact_block: hsi.clone(),
}))
});
if queued {
break;
}
if tokio::time::Instant::now() >= writer_deadline {
panic!(
"seed inbound writer must take cmpctblock (seed={:?} peer={:?})",
seed.peers.snapshot(),
peer.peers.snapshot()
);
}
tokio::time::sleep(Duration::from_millis(20)).await;
}

let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
loop {
let getblocktxn = seed
.peers
.snapshot()
.into_iter()
.find(|p| p.inbound)
.map(|p| p.bytesrecv_per_msg.get("getblocktxn").copied().unwrap_or(0))
.unwrap_or(0);
if getblocktxn > 0 {
break;
}
if tokio::time::Instant::now() >= deadline {
panic!(
"follower must GetBlockTxn the missing extra tx \
(seed={:?} peer={:?})",
seed.peers.snapshot(),
peer.peers.snapshot()
);
}
tokio::time::sleep(Duration::from_millis(20)).await;
}

seed.shutdown().await;
peer.shutdown().await;
};
tokio::time::timeout(Duration::from_secs(20), fut)
.await
.expect("p2p_compact_getblocktxn_missing_extra_tx wall timeout (20s)");
}

/// Phase 4: seeder restarts with empty RAM cache; peer IBD-syncs via reconstruct
/// (CI **multinode** job only — `coverage.sh` also skips this name).
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
Expand Down