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
4 changes: 2 additions & 2 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ Prefer **one high-level scenario** per behavior cluster. Delete lower-level test
| `consensus_mature_chain_spend_reconstruct_and_scripthash` | Consensus+query | **One** mature mine: spend, local prev_fk, double-spend, reopen reconstruct (`witness_block_bytes` == serialize), SH history/balance |
| `ibd_parallel_archive_idempotent_confirm_without_tx_head` | Query+consensus | Out-of-order archive, re-archive idempotent, head-off prevout+maturity |
| `resume_head_off_warms_cache_for_external_prev` | Query+consensus | Resume head-off: warm Class A cache fixes external-prev missing prevout |
| `consensus_rules` (test binary) | Consensus | Focused reject paths for structure/header/connect rules we own — see [`docs/consensus-tests.md`](./docs/consensus-tests.md). Hornet-mapped subset: `./scripts/test-hornet-rules.sh` |
| `consensus_rules` (test binary) | Consensus | Focused reject paths for structure/header/connect rules we own — see [`docs/consensus-tests.md`](./docs/consensus-tests.md). Combined `header_and_spending_boundaries` includes H1/H2/H5. Hornet-mapped subset: `./scripts/test-hornet-rules.sh` |
| `core_analogs::analog_milestone_and_mempool_persist` | Consensus | Milestone skip-below/check-above, missing prevout under high milestone, mempool persist (one pad) |
| `core_analogs::analog_reconstruct_after_lost_head` | Store+query | Wipe `tx.head/`, reopen, reconstruct height 1 and txid probe. Does **not** pin empty-head / truncated-head / v1 fuse refuse |
| `unified_wire_pipeline_multi_block_to_tip` | Consensus+query | Class A archived ahead of tip then `confirm_wire_run` (no re-append + re-entry); then heights 2..=4 unified load/scripts/write |
Expand All @@ -237,7 +237,7 @@ Prefer **one high-level scenario** per behavior cluster. Delete lower-level test
| `electrum_tweaks_subscribe_streams_then_done` | Electrum | Cake `tweaks.subscribe`: one-height result, per-height notifies, then `done` |
| `electrum_max_connections_rejects_extra_client` | Electrum | TCP cap drops the extra client |
| `electrum_idle_timeout_disconnects_quiet_client` | Electrum | Idle timeout closes a quiet socket |
| `esplora_broadcast_visible_in_rpc_and_electrum` | Node + Electrum + Esplora + RPC | One `run_p2p` datadir: HTTP `sendrawtransaction` / `testmempoolaccept` (allowed + missing-or-spent), Esplora `POST /tx` parent and mempool child appear in `getrawmempool` and Electrum mempool/history (`fee` on unconfirmed, including child `height = -1`). Keep `accept.rs` reject units and RPC dry-run orphan-count |
| `esplora_broadcast_visible_in_rpc_and_electrum` | Node + Electrum + Esplora + RPC | One `run_p2p` datadir: HTTP `sendrawtransaction` / `testmempoolaccept` (allowed, missing-or-spent, min-relay), Esplora `POST /tx` parent and mempool child appear in `getrawmempool` and Electrum mempool/history (`fee` on unconfirmed, including child `height = -1`); `generate` includes those txs; immature coinbase sendraw rejects. Keep `accept.rs` reject units and RPC dry-run orphan-count |
| `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`, full-relay GetAddr cache 1000, headers-sync stall replace, self-connect refuses, AddrFetch `getaddr`/`addrv2` (no `getheaders`), one keepalive ping/pong. Sole-preferred stall KEEP stays a PeerHub unit. |
| `p2p_compact_hb_getblocktxn_and_orphan` | P2P (**default**) | One mature pad: HB coinbase `cmpctblock`, 2-tx compact → `getblocktxn` + connect, orphan child GetData then parent accept (INV AlreadyHave). Does **not** pin depth-10 full-block serve, tokio-worker lock, or park-not-reject logs |
Expand Down
106 changes: 30 additions & 76 deletions crates/rbitcoin-consensus/tests/script_edge_fixtures.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Consensus script-edge regression fixtures (signet + mainnet wire blocks).
//!
//! One integration binary keeps link/build cost low; each `#[test]` still pins a
//! unique hash / opcode / verify path (see docs/consensus-tests.md and comments).
//! Captured blocks are not Core JSON rows. One table pins hash / opcode
//! presence; detached verify stays for the two script-engine edges.

use bitcoin::consensus::deserialize;
use bitcoin::script::ScriptBuf;
Expand All @@ -25,72 +25,50 @@ fn hex_bytes(s: &str) -> Vec<u8> {
.collect()
}

// ── signet 200001: OP_CHECKSIGADD (0xba) ─────────────────────────────────────
fn block_contains_byte(b: &Block, needle: u8) -> bool {
for tx in &b.txdata {
for input in &tx.input {
for i in 0..input.witness.len() {
if input.witness.nth(i).unwrap_or(&[]).contains(&needle) {
return true;
}
}
if input.script_sig.as_bytes().contains(&needle) {
return true;
}
}
for o in &tx.output {
if o.script_pubkey.as_bytes().contains(&needle) {
return true;
}
}
}
false
}

#[test]
fn block_200001_deserializes_and_matches_reject_hash() {
fn captured_signet_blocks_match_hashes_and_opcodes() {
let b = load_block("signet_block_200001.bin");
assert_eq!(b.txdata.len(), 321);
assert_eq!(
format!("{}", b.block_hash()),
"000000ad6bf1ea934186822de99a611924d94aff8fbcb1ad6be2c790c3b92ae1"
);
}

#[test]
fn block_200001_witnesses_contain_opcode_0xba() {
let b = load_block("signet_block_200001.bin");
let mut found = false;
for tx in &b.txdata {
for input in &tx.input {
for i in 0..input.witness.len() {
let item = input.witness.nth(i).unwrap_or(&[]);
if item.contains(&0xba) {
found = true;
}
}
}
}
assert!(
found,
"expected 0xba (OP_CHECKSIGADD) in some witness element of block 200001"
block_contains_byte(&b, 0xba),
"expected 0xba (OP_CHECKSIGADD) in block 200001"
);
}

// ── signet 200945: OP_1SUB (0x8c) ────────────────────────────────────────────

#[test]
fn block_200945_has_op_1sub_and_matches_hash() {
let b = load_block("signet_block_200945.bin");
assert_eq!(
format!("{}", b.block_hash()),
"00000065c6d2d4cb574038892a535c50efd66f28265a6ab4c48bd121fef795f7"
);
let mut found = false;
for tx in &b.txdata {
for input in &tx.input {
for i in 0..input.witness.len() {
if input.witness.nth(i).unwrap_or(&[]).contains(&0x8c) {
found = true;
}
}
if input.script_sig.as_bytes().contains(&0x8c) {
found = true;
}
}
for o in &tx.output {
if o.script_pubkey.as_bytes().contains(&0x8c) {
found = true;
}
}
}
assert!(found, "expected 0x8c (OP_1SUB) in block 200945");
}

// ── signet 201393: large tapscript (>10k) ────────────────────────────────────
assert!(
block_contains_byte(&b, 0x8c),
"expected 0x8c (OP_1SUB) in block 200945"
);

#[test]
fn block_201393_has_witness_script_over_10k() {
let b = load_block("signet_block_201393.bin");
assert_eq!(
format!("{}", b.block_hash()),
Expand All @@ -108,35 +86,20 @@ fn block_201393_has_witness_script_over_10k() {
max_item > 10_000,
"expected a witness item >10k (tapscript leaf); max={max_item}"
);
}

// ── signet 204802: P2SH multi-push ───────────────────────────────────────────

#[test]
fn block_204802_matches_reject_hash() {
let b = load_block("signet_block_204802.bin");
assert_eq!(
format!("{}", b.block_hash()),
"0000004273035bc6ed29b7197e9c7615da498baeedb7d9e1c5edb4479de7ecc4"
);
}

// ── signet 219477: P2SH cleanstack ───────────────────────────────────────────

#[test]
fn block_219477_matches_reject_hash() {
let b = load_block("signet_block_219477.bin");
assert_eq!(
format!("{}", b.block_hash()),
"000000d59c5d06312f71cd887a500cfb3ecdfd8563c5205c4a075ac33ae08fbc"
);
assert!(b.txdata.len() > 1);
}

// ── signet 277442: CODESEPARATOR + P2WSH CSV ──────────────────────────────────

#[test]
fn block_277442_matches_reject_hash() {
let b = load_block("signet_block_277442.bin");
assert_eq!(
format!("{}", b.block_hash()),
Expand All @@ -154,24 +117,17 @@ fn block_277442_matches_reject_hash() {
);
}

// ── signet 90719: BIP342 CODESEPARATOR tapscript ─────────────────────────────

const BLOCK_90719_HASH: &str = "000001425fa8c62dfd856ae0fee3b36add930a5826778f62c54c5e7a089cb2cd";
const SPEND_90719_TXID: &str = "179341698633641e6079171f4a61eb1fe203611df3618e717951f2636a7c5481";
const PREV_90719_VALUE: u64 = 99_639;
const PREV_90719_SPK_HEX: &str =
"5120141cf362a850f2bca99e43abca8783cf5db18baadfef55b9769ea285da326c9f";

#[test]
fn block_90719_matches_reject_hash() {
fn block_90719_codeseparator_tapscript_verifies() {
let b = load_block("signet_block_90719.bin");
assert_eq!(format!("{}", b.block_hash()), BLOCK_90719_HASH);
assert_eq!(b.txdata.len(), 14);
}

#[test]
fn block_90719_codeseparator_tapscript_verifies() {
let b = load_block("signet_block_90719.bin");
let tx = b
.txdata
.iter()
Expand Down Expand Up @@ -220,8 +176,6 @@ fn block_90719_codeseparator_tapscript_verifies() {
.expect("BIP342 CODESEPARATOR tapscript must verify");
}

// ── mainnet 290329: P2SH FindAndDelete ───────────────────────────────────────

const MAINNET_290329: &[u8] = include_bytes!("fixtures/mainnet_block_290329.bin");
const FAIL_TXID_290329: &str = "5df1375ffe61ac35ca178ebb0cab9ea26dedbd0e96005dfcee7e379fa513232f";

Expand Down
79 changes: 22 additions & 57 deletions crates/rbitcoin-test/tests/consensus_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,41 +27,6 @@ fn connect_genesis(q: &Query, params: &ChainParams) {

// ─── Header rules ───────────────────────────────────────────────────────────

#[test]
fn h1_rejects_wrong_genesis_hash() {
let (_td, q, params) = regtest_q();
let mut g = regtest_genesis();
g.header.nonce = g.header.nonce.wrapping_add(1);
// Even if PoW happens to pass regtest, genesis hash check fires first for h=0.
let err = validate_header(&q, &params, Height::GENESIS, &g.header).unwrap_err();
assert!(
matches!(err, ConsensusError::BadHeader(s) if s.contains("genesis")),
"{err:?}"
);
}

#[test]
fn h2_rejects_bad_prev_link() {
let (_td, q, params) = regtest_q();
connect_genesis(&q, &params);
let g = regtest_genesis();
let mut b1 = mine_regtest_block(g.block_hash(), g.header.time + 1, 1, vec![]);
b1.header.prev_blockhash = BlockHash::from_byte_array([0xee; 32]);
// Re-mine nonce after prev change (PoW may fail first; BadPrev is the link check).
let target = bitcoin::Target::from_compact(b1.header.bits);
for nonce in 0..100_000u32 {
b1.header.nonce = nonce;
if b1.header.validate_pow(target).is_ok() {
break;
}
}
let err = validate_header(&q, &params, Height(1), &b1.header).unwrap_err();
assert!(
matches!(err, ConsensusError::BadPrev),
"expected BadPrev, got {err:?}"
);
}

#[test]
fn h4_rejects_checkpoint_mismatch() {
let (_td, q, mut params) = regtest_q();
Expand All @@ -80,28 +45,6 @@ fn h4_rejects_checkpoint_mismatch() {
);
}

#[test]
fn h5_regtest_rejects_wrong_bits() {
let (_td, q, params) = regtest_q();
connect_genesis(&q, &params);
let g = regtest_genesis();
let mut b1 = mine_regtest_block(g.block_hash(), g.header.time + 600, 1, vec![]);
// Corrupt bits (regtest has no retarget — must equal prev).
b1.header.bits = CompactTarget::from_consensus(0x207f_fffe);
let target = bitcoin::Target::from_compact(b1.header.bits);
for nonce in 0..100_000u32 {
b1.header.nonce = nonce;
if b1.header.validate_pow(target).is_ok() {
break;
}
}
let err = validate_header(&q, &params, Height(1), &b1.header).unwrap_err();
assert!(
matches!(err, ConsensusError::BadHeader(s) if s.contains("bits") || s.contains("proof")),
"{err:?}"
);
}

#[test]
fn h6_target_above_pow_limit_is_detectable() {
// We reject `target > pow_limit` in validate_header; assert the comparison
Expand Down Expand Up @@ -192,8 +135,30 @@ fn header_and_spending_boundaries() {

let (_td, q, params) = regtest_q();
let g = regtest_genesis();
let mut bad_g = g.clone();
bad_g.header.nonce = g.header.nonce.wrapping_add(1);
let err = validate_header(&q, &params, Height::GENESIS, &bad_g.header).unwrap_err();
assert!(
matches!(err, ConsensusError::BadHeader(s) if s.contains("genesis")),
"h1: {err:?}"
);
accept_and_connect_block(&q, &params, Height::GENESIS, &g, Milestone::NONE).unwrap();

let mut bad_prev = mine_regtest_block(g.block_hash(), g.header.time + 1, 1, vec![]);
bad_prev.header.prev_blockhash = BlockHash::from_byte_array([0xee; 32]);
grind_pow(&mut bad_prev);
let err = validate_header(&q, &params, Height(1), &bad_prev.header).unwrap_err();
assert!(matches!(err, ConsensusError::BadPrev), "h2: {err:?}");

let mut bad_bits = mine_regtest_block(g.block_hash(), g.header.time + 600, 1, vec![]);
bad_bits.header.bits = CompactTarget::from_consensus(0x207f_fffe);
grind_pow(&mut bad_bits);
let err = validate_header(&q, &params, Height(1), &bad_bits.header).unwrap_err();
assert!(
matches!(err, ConsensusError::BadHeader(s) if s.contains("bits") || s.contains("proof")),
"h5: {err:?}"
);

let b1 = mine_regtest_block(g.block_hash(), g.header.time + 600, 1, vec![]);
validate_header(&q, &params, Height(1), &b1.header).expect("valid parent, pow, bits");
accept_and_connect_block(&q, &params, Height(1), &b1, Milestone::NONE).unwrap();
Expand Down
66 changes: 66 additions & 0 deletions crates/rbitcoin-test/tests/cross_surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use rbitcoin_query::Query;
use rbitcoin_test::TestDatadir;
use serde_json::{json, Value};
use std::net::SocketAddr;
use std::str::FromStr;
use std::time::{Duration, Instant};
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
Expand Down Expand Up @@ -183,6 +184,17 @@ async fn esplora_broadcast_visible_in_rpc_and_electrum() {
rpc_spend.consensus_encode(&mut rpc_raw).unwrap();
let rpc_hex = rbitcoin_primitives::hex_encode(&rpc_raw);
let rpc_txid = rpc_spend.compute_txid().to_string();
let mut zero_fee = rpc_spend.clone();
zero_fee.output[0].value = Amount::from_sat(50_0000_0000);
let mut zero_raw = Vec::new();
zero_fee.consensus_encode(&mut zero_raw).unwrap();
let zero_hex = rbitcoin_primitives::hex_encode(&zero_raw);
let tma = jsonrpc(rpc_addr, "testmempoolaccept", json!([[zero_hex]])).await;
assert_eq!(tma["result"][0]["allowed"], false, "{tma}");
assert_eq!(
tma["result"][0]["reject-reason"], "min relay fee not met",
"{tma}"
);
let tma = jsonrpc(rpc_addr, "testmempoolaccept", json!([[rpc_hex.clone()]])).await;
assert_eq!(tma["result"][0]["allowed"], true, "{tma}");
let sent = jsonrpc(rpc_addr, "sendrawtransaction", json!([rpc_hex])).await;
Expand Down Expand Up @@ -359,6 +371,60 @@ async fn esplora_broadcast_visible_in_rpc_and_electrum() {
"{child_mem_row}"
);

let mined = jsonrpc(rpc_addr, "generate", json!([1])).await;
assert_eq!(
mined["result"].as_array().map(|a| a.len()),
Some(1),
"{mined}"
);
let count = jsonrpc(rpc_addr, "getblockcount", json!([])).await;
assert_eq!(count["result"], 103, "{count}");
let empty = jsonrpc(rpc_addr, "getrawmempool", json!([])).await;
assert_eq!(empty["result"], json!([]), "{empty}");
let tip = jsonrpc(rpc_addr, "getbestblockhash", json!([])).await;
let blk = jsonrpc(rpc_addr, "getblock", json!([tip["result"].clone(), 2])).await;
let txs = blk["result"]["tx"].as_array().expect("mined tx array");
assert!(
txs.len() >= 4,
"coinbase + sendraw + esplora parent + child: {blk}"
);
assert!(
txs.iter().any(|t| t["txid"] == rpc_txid),
"generate must include sendraw: {blk}"
);
let cb_txid = txs[0]["txid"].as_str().expect("coinbase txid").to_string();
let cb_val = (txs[0]["vout"][0]["value"].as_f64().unwrap() * 100_000_000.0).round() as u64;
let immature = Transaction {
version: TxVersion::TWO,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint {
txid: Txid::from_str(&cb_txid).expect("coinbase txid"),
vout: 0,
},
script_sig: ScriptBuf::new(),
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
witness: Witness::new(),
}],
output: vec![TxOut {
value: Amount::from_sat(cb_val.saturating_sub(1_000)),
script_pubkey: ScriptBuf::from_bytes(vec![0x51]),
}],
};
let mut imm_raw = Vec::new();
immature.consensus_encode(&mut imm_raw).unwrap();
let imm = jsonrpc(
rpc_addr,
"sendrawtransaction",
json!([rbitcoin_primitives::hex_encode(&imm_raw)]),
)
.await;
assert_eq!(imm["error"]["code"], -26, "{imm}");
assert_eq!(
imm["error"]["message"], "bad-txns-premature-spend-of-coinbase",
"{imm}"
);

let _ = jsonrpc(rpc_addr, "stop", json!([])).await;
let stopped = tokio::time::timeout(Duration::from_secs(15), node).await;
match stopped {
Expand Down
Loading