From 4a41784b8b0e5d6878ea95252d0c6108d3100a00 Mon Sep 17 00:00:00 2001 From: rbitcoin-grok Date: Sat, 12 Sep 2026 22:36:15 -0700 Subject: [PATCH 1/2] test: pin Signet genesis and height-1 on run_node smoke run_node already looped Signet with no header. Connect genesis plus the captured height-1 block so BIP325 tip-confirm is a process journey, then drop the solution-valid unit. Keep mutated-solution and encode guts. Co-authored-by: Cursor --- TESTING.md | 2 +- crates/rbitcoin-consensus/src/signet.rs | 17 ----------- crates/rbitcoin-test/tests/scenarios.rs | 39 +++++++++++++++++++++++-- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/TESTING.md b/TESTING.md index 8c6007984..3b5939fd4 100644 --- a/TESTING.md +++ b/TESTING.md @@ -215,7 +215,7 @@ Prefer **one high-level scenario** per behavior cluster. Delete lower-level test | ID | Layer | Description | |----|-------|-------------| -| `node_cli_and_surface_smoke` | Lifecycle/CLI | Networks, `run_node`, config errors, CLI flags (incl. `--conf`, `--peertimeout=0`, log-level/mempool/electrum/inhibit), help/version | +| `node_cli_and_surface_smoke` | Lifecycle/CLI | Networks, `run_node`, config errors, CLI flags (incl. `--conf`, `--peertimeout=0`, log-level/mempool/electrum/inhibit), help/version. Signet: genesis header plus height-1 BIP325 connect | | `three_stage_confirm_and_parent_pin_surface` | Consensus+query | Split load→scripts→write; parent pin; load ready timeout/cancel; instance-owned `last_write` / `last_pin` / `take_window` meters | | `block_cache_and_mempool_hub_surface` | Net | BlockCache locator/eviction + MempoolHub accept/remove/reorg on mature chain. `DEFAULT_BODY_DEPTH == 16` stays a unit. | | `store_error_and_corrupt_paths` | Store | Error/corrupt surfaces | diff --git a/crates/rbitcoin-consensus/src/signet.rs b/crates/rbitcoin-consensus/src/signet.rs index 3d80b9349..b4502edfa 100644 --- a/crates/rbitcoin-consensus/src/signet.rs +++ b/crates/rbitcoin-consensus/src/signet.rs @@ -400,23 +400,6 @@ mod tests { assert_eq!(stripped[0], 0x6a); } - /// Regression: height-1 global signet block must accept under BIP325. - /// - /// Bug class: `to_spend.scriptSig` missing leading `OP_0` before block_data push - /// produced a wrong txid → CHECKMULTISIG failed → tip stuck at 0. - #[test] - fn signet_block_1_solution_valid() { - let raw = include_bytes!("../tests/fixtures/signet_block_1.bin"); - let block: Block = deserialize(raw).expect("decode signet block 1"); - assert_eq!( - block.header.block_hash().to_string(), - "00000086d6b2636cb2a392d45edc4ec544a10024d30141c9adf4bfd9de533b53" - ); - let challenge = default_signet_challenge(); - validate_signet_block_solution(&block, challenge.as_script()) - .expect("BIP325 solution for real signet height 1"); - } - #[test] fn custom_challenge_derives_expected_wire_magic() { let challenge = ScriptBuf::from_bytes(vec![0x51]); diff --git a/crates/rbitcoin-test/tests/scenarios.rs b/crates/rbitcoin-test/tests/scenarios.rs index 8231f7fc6..32165c03d 100644 --- a/crates/rbitcoin-test/tests/scenarios.rs +++ b/crates/rbitcoin-test/tests/scenarios.rs @@ -3,10 +3,11 @@ //! Prefer fewer tests at the highest layer that still hit production paths. //! Mature regtest chains are built once per test that needs them (not thrice). +use bitcoin::consensus::encode::deserialize; use bitcoin::hashes::Hash; -use bitcoin::{Amount, BlockHash}; +use bitcoin::{Amount, Block, BlockHash}; use rbitcoin_cli::cli_main as cli_cli_main; -use rbitcoin_consensus::{accept_and_connect_block, ChainParams, Milestone}; +use rbitcoin_consensus::{accept_and_connect_block, genesis_block, ChainParams, Milestone}; use rbitcoin_node::{cli_main as node_cli_main, run_node, NodeConfig}; use rbitcoin_primitives::{Fk, Height, Network, VERSION}; use rbitcoin_query::testutil::FixtureChain; @@ -16,6 +17,7 @@ use rbitcoin_test::mine::{mine_regtest_block, regtest_genesis, spend_anyone_can_ use rbitcoin_test::{ assert_reconstruct_eq, build_mature_regtest_with_spend, pad_empty_from, TestDatadir, }; +use std::path::PathBuf; use std::process::{Command, ExitCode}; /// This toolchain's `ExitCode` lacks `PartialEq`; compare via Debug. @@ -42,6 +44,39 @@ fn node_cli_and_surface_smoke() { .with_tiny_heads(); let handle = run_node(cfg).unwrap(); assert_eq!(handle.network_name(), net.as_str()); + if net == Network::Signet { + let params = ChainParams::signet(); + let genesis = genesis_block(¶ms); + handle.query.enter_direct_index_mode().unwrap(); + accept_and_connect_block( + &handle.query, + ¶ms, + Height::GENESIS, + &genesis, + Milestone::NONE, + ) + .unwrap(); + let (_fk, rec) = handle + .query + .header_at_height(Height::GENESIS) + .unwrap() + .expect("signet genesis header"); + assert_eq!(rec.hash, genesis.block_hash().to_byte_array()); + assert_eq!(rec.hash, params.genesis_hash.to_byte_array()); + let raw = std::fs::read( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../rbitcoin-consensus/tests/fixtures/signet_block_1.bin"), + ) + .expect("signet_block_1.bin"); + let block1: Block = deserialize(&raw).expect("signet height 1"); + assert_eq!( + block1.block_hash().to_string(), + "00000086d6b2636cb2a392d45edc4ec544a10024d30141c9adf4bfd9de533b53" + ); + accept_and_connect_block(&handle.query, ¶ms, Height(1), &block1, Milestone::NONE) + .unwrap(); + assert_eq!(handle.query.tip_height(), Some(Height(1))); + } handle.shutdown().unwrap(); } assert!(Network::parse("nope").is_err()); From 713658d5c2fd08616555fd01ac8925c36998d9a9 Mon Sep 17 00:00:00 2001 From: rbitcoin-grok Date: Sat, 12 Sep 2026 22:36:25 -0700 Subject: [PATCH 2/2] test: sendrawtransaction and testmempoolaccept on process RPC The cross-surface pad already had JSON-RPC and Esplora POST /tx. Drive sendraw of a second mature spend, testmempoolaccept allowed and missing-or-spent, and sendraw of a live mempool tx. Keep accept.rs rejects and the RPC dry-run orphan-count pin. Co-authored-by: Cursor --- TESTING.md | 2 +- crates/rbitcoin-test/tests/cross_surface.rs | 80 +++++++++++++++++++-- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/TESTING.md b/TESTING.md index 3b5939fd4..820c6682d 100644 --- a/TESTING.md +++ b/TESTING.md @@ -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: Esplora `POST /tx` parent and mempool child appear in `getrawmempool` and Electrum mempool/history (`fee` on unconfirmed, including child `height = -1`) | +| `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 | | `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 | diff --git a/crates/rbitcoin-test/tests/cross_surface.rs b/crates/rbitcoin-test/tests/cross_surface.rs index c49983e57..eef4e46a1 100644 --- a/crates/rbitcoin-test/tests/cross_surface.rs +++ b/crates/rbitcoin-test/tests/cross_surface.rs @@ -2,9 +2,10 @@ use bitcoin::absolute::LockTime; use bitcoin::consensus::Encodable; +use bitcoin::hashes::Hash; use bitcoin::script::ScriptBuf; use bitcoin::transaction::Version as TxVersion; -use bitcoin::{Amount, OutPoint, Sequence, Transaction, TxIn, TxOut, Witness}; +use bitcoin::{Amount, OutPoint, Sequence, Transaction, TxIn, TxOut, Txid, Witness}; use rbitcoin_consensus::{accept_and_connect_block, pad_empty_from, ChainParams, Milestone}; use rbitcoin_electrum::electrum_scripthash_hex; use rbitcoin_node::{run_p2p, NodeConfig}; @@ -116,7 +117,7 @@ async fn esplora_broadcast_visible_in_rpc_and_electrum() { let td = TestDatadir::new().unwrap(); let params = ChainParams::regtest(); let genesis = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest); - let coinbase_txid = { + let (coinbase_txid, rpc_cb) = { let q = Query::open_or_create_tiny(td.store_path()).unwrap(); accept_and_connect_block(&q, ¶ms, Height::GENESIS, &genesis, Milestone::NONE).unwrap(); let (_tip, _time, cbs) = pad_empty_from( @@ -125,11 +126,11 @@ async fn esplora_broadcast_visible_in_rpc_and_electrum() { genesis.block_hash(), genesis.header.time, 1, - 101, - 1, + 102, + 2, ); q.flush().unwrap(); - cbs[0] + (cbs[0], cbs[1]) }; let electrum_addr = ephemeral_addr(); @@ -156,9 +157,68 @@ async fn esplora_broadcast_visible_in_rpc_and_electrum() { let (st, height) = http_get(esplora_addr, "/blocks/tip/height").await; assert_eq!(st, 200, "esplora tip height: {height}"); - assert_eq!(height, "101"); + assert_eq!(height, "102"); let count = jsonrpc(rpc_addr, "getblockcount", json!([])).await; - assert_eq!(count["result"], 101, "{count}"); + assert_eq!(count["result"], 102, "{count}"); + + let rpc_spk = ScriptBuf::from_bytes(vec![0x54]); + let rpc_spend = Transaction { + version: TxVersion::TWO, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: rpc_cb, + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::from_sat(50_0000_0000 - 1_000), + script_pubkey: rpc_spk, + }], + }; + let mut rpc_raw = Vec::new(); + 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 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; + assert_eq!(sent["result"], rpc_txid, "{sent}"); + let mem = jsonrpc(rpc_addr, "getrawmempool", json!([])).await; + let ids = mem["result"].as_array().expect("getrawmempool array"); + assert!( + ids.iter().any(|v| v.as_str() == Some(rpc_txid.as_str())), + "getrawmempool missing sendraw {rpc_txid}: {mem}" + ); + let miss = Transaction { + version: TxVersion::TWO, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: Txid::from_byte_array([0x11; 32]), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::from_sat(1), + script_pubkey: ScriptBuf::from_bytes(vec![0x51]), + }], + }; + let mut miss_raw = Vec::new(); + miss.consensus_encode(&mut miss_raw).unwrap(); + let miss_hex = rbitcoin_primitives::hex_encode(&miss_raw); + let tma = jsonrpc(rpc_addr, "testmempoolaccept", json!([[miss_hex]])).await; + assert_eq!(tma["result"][0]["allowed"], false, "{tma}"); + assert_eq!( + tma["result"][0]["reject-reason"], "bad-txns-inputs-missingorspent", + "{tma}" + ); let spk = ScriptBuf::from_bytes(vec![0x52]); let spend = Transaction { @@ -186,6 +246,8 @@ async fn esplora_broadcast_visible_in_rpc_and_electrum() { let (st, body) = http_post(esplora_addr, "/tx", &hex).await; assert_eq!(st, 200, "POST /tx: {body}"); assert_eq!(body, txid_hex); + let dup = jsonrpc(rpc_addr, "sendrawtransaction", json!([hex.clone()])).await; + assert_eq!(dup["result"], txid_hex, "sendraw of live mempool tx: {dup}"); let (st, status) = http_get(esplora_addr, &format!("/tx/{txid_hex}/status")).await; assert_eq!(st, 200, "GET /tx status: {status}"); @@ -198,6 +260,10 @@ async fn esplora_broadcast_visible_in_rpc_and_electrum() { ids.iter().any(|v| v.as_str() == Some(txid_hex.as_str())), "getrawmempool missing {txid_hex}: {mem}" ); + assert!( + ids.iter().any(|v| v.as_str() == Some(rpc_txid.as_str())), + "getrawmempool dropped sendraw {rpc_txid}: {mem}" + ); let sh = electrum_scripthash_hex(spk.as_bytes()); let mut el = TcpStream::connect(electrum_addr).await.unwrap();