Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0783ce5
deps(node): drop unused proptest dev-dependency
metaphorics Oct 2, 2026
7ae0000
harness: collapse clean_stderr #[path] include into e2e::helpers
metaphorics Oct 2, 2026
34635b0
deps(node): collapse redundant feature unification edges
metaphorics Oct 2, 2026
a4529ec
harness: dedup required_str/required_u64 onto e2e ValueExt
metaphorics Oct 2, 2026
c862d1c
harness: demote WorkspaceGraph graph fields to private
metaphorics Oct 2, 2026
4ee7897
scripts: repair import_qa_assets COMMANDS parsing for the &[&str] inv…
metaphorics Oct 2, 2026
7c194dc
clippy.toml: drop disallowed-types config for a never-enabled lint
metaphorics Oct 2, 2026
c5ccadc
install-bitcoind.sh: drop the unreferenced --export mode
metaphorics Oct 2, 2026
eafdb23
deny.toml: drop the empty advisories ignore list
metaphorics Oct 2, 2026
1c8572a
docs(policies): point download-budget refs at download_window/policy.rs
metaphorics Oct 2, 2026
42d96be
docs(rpc): name on_connect as the C-event publisher
metaphorics Oct 2, 2026
807a7da
Merge remote-tracking branch 'origin/devin/purge7-docs-380df47a' into…
metaphorics Oct 2, 2026
1c48a2e
Merge remote-tracking branch 'origin/devin/purge7-tooling-380df47a' i…
metaphorics Oct 2, 2026
4ce7f3f
Merge remote-tracking branch 'origin/devin/purge7-harness-380df47a' i…
metaphorics Oct 2, 2026
354c23e
docs(node): drop stale prometheus-http feature claim
metaphorics Oct 2, 2026
76367e4
scripts: mask char literals; restore install-bitcoind --export mode
metaphorics Oct 2, 2026
b4da894
scripts: keep command selectors stable across raw-string COMMANDS ent…
metaphorics Oct 2, 2026
1b971ec
e2e: mirror captured child output to the evidence file as it streams
metaphorics Oct 2, 2026
8162b60
scripts: parse COMMANDS entries as complete literals; e2e: amortize t…
metaphorics Oct 2, 2026
19b267f
e2e: compact capture file to exact tail at EOF
metaphorics Oct 2, 2026
6db74e6
e2e: publish compacted tail atomically via rename
metaphorics Oct 3, 2026
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
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions bin/bitcoin-rs/tests/head_sync_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,17 @@

#![expect(clippy::expect_used, reason = "process test assertions")]

#[path = "support/clean_stderr.rs"]
mod clean_stderr;

use std::time::{Duration, Instant};

use bitcoin::p2p::message::NetworkMessage;
use bitcoin::p2p::message_blockdata::Inventory;
use bitcoin_rs_e2e::helpers::{
best_hash, block_count, build_chain, connection_count, genesis_block, wait_for,
assert_clean_stderr, best_hash, block_count, build_chain, connection_count, genesis_block,
wait_for,
};
use bitcoin_rs_e2e::live_peer::LivePeer;
use bitcoin_rs_e2e::live_peer::pump_until_tip;
use bitcoin_rs_e2e::{Error, Kind, ProcessNode};
use clean_stderr::assert_clean_stderr;

/// T1+T2: a block announced by `inv` is availability, not a body order: the
/// node fetches headers first, and the admitted tip's body rides a window
Expand Down
7 changes: 2 additions & 5 deletions bin/bitcoin-rs/tests/live_head_carried_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,17 @@

#![expect(clippy::expect_used, reason = "process test assertions")]

#[path = "support/clean_stderr.rs"]
mod clean_stderr;

use std::time::{Duration, Instant};

use bitcoin::p2p::message::NetworkMessage;
use bitcoin::p2p::message_blockdata::Inventory;
use bitcoin_rs_e2e::helpers::{
best_hash, block_count, build_chain, connection_count, genesis_block, wait_for,
assert_clean_stderr, best_hash, block_count, build_chain, connection_count, genesis_block,
wait_for,
};
use bitcoin_rs_e2e::live_peer::LivePeer;
use bitcoin_rs_e2e::live_peer::pump_until_tip;
use bitcoin_rs_e2e::{Error, Kind, ProcessNode};
use clean_stderr::assert_clean_stderr;

/// Pumps until a `getdata` requests `hash` (serving every request
/// type-faithfully), up to `dur`. Returns true when the request was seen.
Expand Down
28 changes: 7 additions & 21 deletions bin/bitcoin-rs/tests/overhaul_external_miner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use bitcoin::consensus::encode::serialize_hex;
use bitcoin_rs_e2e::helpers::{
COINBASE_MATURITY, assemble_block_from_template, mature_funding, op_true_script, spend_anyone,
};
use bitcoin_rs_e2e::{Kind, ProcessNode};
use bitcoin_rs_e2e::{Kind, ProcessNode, ValueExt};
use serde_json::{Value, json};

const FEE_SATS: u64 = 10_000;
Expand All @@ -35,7 +35,7 @@ fn external_miner_assembles_template_and_submits_block() -> TestResult {

let template = node.rpc("getblocktemplate", &json!([{"rules": ["segwit"]}]))?;
assert_eq!(
required_u64(&template, "height")?,
template.u64_field("height")?,
u64::from(COINBASE_MATURITY) + 2,
"template must extend the current tip"
);
Expand All @@ -50,17 +50,17 @@ fn external_miner_assembles_template_and_submits_block() -> TestResult {
);
let entry = &template_txs[0];
assert_eq!(
required_str(entry, "hash")?,
entry.str_field("hash")?,
spend.compute_wtxid().to_string(),
"rendered hash must be the spend wtxid"
);
assert_eq!(
required_u64(entry, "fee")?,
entry.u64_field("fee")?,
FEE_SATS,
"rendered fee must match the spend fee"
);
assert!(
required_u64(entry, "weight")? > 0,
entry.u64_field("weight")? > 0,
"rendered weight must be positive"
);
let depends = entry
Expand All @@ -78,7 +78,7 @@ fn external_miner_assembles_template_and_submits_block() -> TestResult {
);

let info = node.rpc("getblockchaininfo", &json!([]))?;
let tip_height = required_u64(&info, "blocks")?;
let tip_height = info.u64_field("blocks")?;
assert_eq!(
tip_height,
u64::from(COINBASE_MATURITY) + 2,
Expand All @@ -87,7 +87,7 @@ fn external_miner_assembles_template_and_submits_block() -> TestResult {

let mempool = node.rpc("getmempoolinfo", &json!([]))?;
assert_eq!(
required_u64(&mempool, "size")?,
mempool.u64_field("size")?,
0,
"mempool must be empty after block inclusion"
);
Expand All @@ -96,17 +96,3 @@ fn external_miner_assembles_template_and_submits_block() -> TestResult {
let _ = node.stop();
Ok(())
}

fn required_str<'a>(value: &'a Value, key: &str) -> TestResult<&'a str> {
value
.get(key)
.and_then(Value::as_str)
.ok_or_else(|| format!("template missing string {key}").into())
}

fn required_u64(value: &Value, key: &str) -> TestResult<u64> {
value
.get(key)
.and_then(Value::as_u64)
.ok_or_else(|| format!("template missing u64 {key}").into())
}
20 changes: 0 additions & 20 deletions bin/bitcoin-rs/tests/support/clean_stderr.rs

This file was deleted.

8 changes: 4 additions & 4 deletions bin/bitcoin-rs/tests/support/dependency_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ struct FeatureDependency {
/// Parsed workspace dependency graph used by the gates.
pub(crate) struct WorkspaceGraph {
/// Normal `bitcoin-rs-*` dependencies per crate.
pub normal_deps: BTreeMap<String, Vec<String>>,
normal_deps: BTreeMap<String, Vec<String>>,
/// Storage engine dependencies per crate.
pub engine_deps: BTreeMap<String, Vec<String>>,
engine_deps: BTreeMap<String, Vec<String>>,
/// External ZMQ implementation dependencies per crate.
pub zmq_deps: BTreeMap<String, Vec<String>>,
zmq_deps: BTreeMap<String, Vec<String>>,
/// Cargo feature implies per crate.
pub features: BTreeMap<String, BTreeMap<String, Vec<String>>>,
features: BTreeMap<String, BTreeMap<String, Vec<String>>>,
/// Feature selections on normal/build workspace edges, excluding dev fixtures.
production_deps: BTreeMap<String, Vec<FeatureDependency>>,
/// Number of workspace packages seen in the metadata.
Expand Down
5 changes: 0 additions & 5 deletions clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,3 @@ msrv = "1.95.0"
cognitive-complexity-threshold = 15
type-complexity-threshold = 250
too-many-arguments-threshold = 8
disallowed-types = [
{ path = "std::sync::Mutex", reason = "use parking_lot::Mutex" },
{ path = "std::sync::RwLock", reason = "use parking_lot::RwLock" },
{ path = "std::collections::HashMap", reason = "use hashbrown::HashMap or hashbrown::HashTable" },
]
14 changes: 6 additions & 8 deletions crates/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,24 @@ workspace = true
# selection: the engine a run uses is `validation.engine` at runtime. The
# default is kernel-free so the C++ toolchain is opt-in.
default = ["fjall", "zmq"]
# Component crates own the storage-backend fan-out: each of chainstate's and
# index's backend features already forwards to storage + utxo under the same
# feature name, so listing storage/utxo here duplicates those edges.
rocksdb = [
"bitcoin-rs-chainstate/rocksdb",
"bitcoin-rs-index/rocksdb",
"bitcoin-rs-storage/rocksdb",
"bitcoin-rs-utxo/rocksdb",
]
fjall = [
"bitcoin-rs-chainstate/fjall",
"bitcoin-rs-index/fjall",
"bitcoin-rs-storage/fjall",
"bitcoin-rs-utxo/fjall",
]
redb = [
"bitcoin-rs-chainstate/redb",
"bitcoin-rs-index/redb",
"bitcoin-rs-storage/redb",
"bitcoin-rs-utxo/redb",
]
kernel = ["bitcoin-rs-chainstate/kernel", "bitcoin-rs-consensus/kernel"]
# chainstate's kernel feature is the kernel-capability propagator for the
# chain stack and already forwards to `bitcoin-rs-consensus/kernel`.
kernel = ["bitcoin-rs-chainstate/kernel"]
zmq = ["bitcoin-rs-rpc/zmq"]

[dependencies]
Expand Down Expand Up @@ -90,7 +89,6 @@ tempfile.workspace = true
bitcoin-rs-chain = { workspace = true, features = ["test-seam"] }
bitcoin-rs-p2p = { workspace = true, features = ["test-seam"] }
bitcoin-rs-chainstate = { workspace = true, features = ["test-seam"] }
proptest.workspace = true
criterion.workspace = true
sonic-rs.workspace = true
serde_json.workspace = true
Expand Down
2 changes: 0 additions & 2 deletions crates/node/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
- `kernel`: compiles bitcoinkernel support in (`bitcoin-rs-consensus/kernel`).
Selection is the runtime `validation.engine` setting (`native` by default);
the feature alone never routes consensus verification to the kernel.
- `prometheus-http`: enables the `metrics-exporter-prometheus/http-listener` feature;
the production listener itself is controlled by `metrics_bind`.

Part of [`bitcoin-rs`](../../README.md); see [`CONCEPTS.md`](../../CONCEPTS.md) for the
project vocabulary.
2 changes: 1 addition & 1 deletion crates/rpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ no backend cargo feature (`g17_dependency_direction` proves both from
### 4. Non-blocking event notifications
- **ZMQ Framing**: ZeroMQ notifications (`ZmqPublisher` in `crates/rpc/src/zmq.rs`) emit 3-part multipart frames `[topic, body, 4-byte LE sequence]`.
- **Non-Blocking Delivery**: Socket writes must use non-blocking sends (`zmq::DONTWAIT`). Notification buffer saturation must drop messages at the high-water mark rather than stalling block validation or consensus execution.
- **Reorg Sequencing & Notification Order**: Chain-transition rollback and admission orchestration in `crates/node/src/chain_effects.rs` guarantees block disconnect events (`D`, published during rollback) are emitted before block connect events (`C`, published by `after_connect`).
- **Reorg Sequencing & Notification Order**: Chain-transition rollback and admission orchestration in `crates/node/src/chain_effects.rs` guarantees block disconnect events (`D`, published during rollback) are emitted before block connect events (`C`, published by `on_connect`).

### 5. Architectural guardrails
- **No Generic Middleware**: Do not introduce heavy async web framework stacks (Axum, Actix, Tower) into `RpcServer`.
Expand Down
1 change: 0 additions & 1 deletion deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ no-default-features = false
[advisories]
version = 2
yanked = "deny"
ignore = []

[licenses]
version = 2
Expand Down
8 changes: 4 additions & 4 deletions docs/policies/p2p-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,14 @@ TXR-09 is the trickled inventory schedule, `m_next_inv_send_time` at
connection policy, no `getaddr` response, and no addr/addrv2 gossip.
Outbound peer discovery runs through DNS-seed bootstrap (on by default:
`run_dns_peer_maintenance`, `crates/p2p/src/service.rs`, seeds from
`Network::dns_seeds`) and the configured `--connect`/`--addnode`
surfaces.
`Network::dns_seeds`), the configured `--connect` peers, and the
`addnode` RPC.
5. **Service bits**: the advertised set follows storage (`init.cpp:2022-2026`): `NETWORK | WITNESS` normally, `NETWORK_LIMITED | WITNESS` when `storage.prune_target_mb > 0`, so a pruned node never claims a full block history. No `NODE_BLOOM` or `NODE_COMPACT_FILTERS` — those services do not exist here.
6. **Timestamp**: `version.timestamp` is always 0 (§4).
7. **Automatic misbehavior bans** (§6) absent; manual bans only.
8. **Chain-sync timeout scope**: a full-relay outbound connection that stops bringing a better chain is timed out as Core does (`ConsiderEviction`, `net_processing.cpp:5498-5550`), with one `getheaders` probe at 20 minutes (`CHAIN_SYNC_TIMEOUT`, definition `net_processing.cpp:109`) and the first four outbound connections to reach the tip protected (`MAX_OUTBOUND_PEERS_TO_PROTECT_FROM_DISCONNECT`, definition `net_processing.cpp:107`). Protection and the timeout both key on a tip the peer actually handed us, never on the height its handshake claimed: Core reads `pindexBestKnownBlock` there, not `nStartingHeight` (use-site `net_processing.cpp:3203-3210`). An operator-pinned connection is exempt in both, as it is in Core: `IsOutboundOrBlockRelayConn()` excludes `ConnectionType::MANUAL` (`net_processing.cpp:5502`). Block-relay-only connections are exempt here; Core times out both outbound classes. A connection dialed for blocks alone is therefore never replaced by this timer.
9. **Download budgets**: bitcoin-rs bounds one sync at `PENDING_BUDGET = 256` in-flight bodies and `RECEIVED_BLOCK_BUDGET = 256` staged bodies (`crates/p2p/src/download_window.rs:56,60`), and stripes at `MAX_BLOCKS_IN_TRANSIT_PER_PEER = 16` once `MIN_PEERS_FOR_FANOUT = 8` eligible peers exist (`:109,118`), where Core runs one `BLOCK_DOWNLOAD_WINDOW = 1024` ahead of the last common block with the same 16 per peer (`net_processing.cpp:151,133`). The 256 depth is measured, not assumed: a bounded 0–150,000 daemon single-peer IBD run at this window was 1.52× the 128-block control (`crates/p2p/src/download_window.rs:52-53`). The shallower window is a bounded divergence kept by operator decision: it caps buffered bodies and re-request work per connection instead of matching Core's depth.
10. **Extra-peer selection**: once a stale tip needs no extra full-relay connection, bitcoin-rs retires the newest automatic full-relay outbound connection that sits one beyond the slots and is older than `MINIMUM_CONNECT_TIME` (`retire_extra_full_relay_connection`, `crates/p2p/src/service.rs:1086`). An operator-pinned connection is outside the count and the victim set both, as in Core: neither `IsFullOutboundConn()` nor `IsBlockOnlyConn()` includes `ConnectionType::MANUAL` (`net_processing.cpp:5558-5604`). Core's `EvictExtraOutboundPeers` (`net_processing.cpp:5604-5668`) instead retires the connection that announced a block longest ago, breaking a tie by dropping the most recently connected one. The retired count is the same; the retired connection is not. A pinned outbound peer therefore neither creates an excess nor stands as a victim, matching `IsFullOutboundConn`/`IsBlockOnlyConn` excluding `ConnectionType::MANUAL` (`net_processing.cpp:5558-5604`).
9. **Download budgets**: bitcoin-rs bounds one sync at `PENDING_BUDGET = 256` in-flight bodies and `RECEIVED_BLOCK_BUDGET = 256` staged bodies (`crates/p2p/src/download_window/policy.rs:54,58`), and stripes at `MAX_BLOCKS_IN_TRANSIT_PER_PEER = 16` once `MIN_PEERS_FOR_FANOUT = 8` eligible peers exist (`:107,116`), where Core runs one `BLOCK_DOWNLOAD_WINDOW = 1024` ahead of the last common block with the same 16 per peer (`net_processing.cpp:151,133`). The 256 depth is measured, not assumed: a bounded 0–150,000 daemon single-peer IBD run at this window was 1.52× the 128-block control (`crates/p2p/src/download_window/policy.rs:50-51`). The shallower window is a bounded divergence kept by operator decision: it caps buffered bodies and re-request work per connection instead of matching Core's depth.
10. **Extra-peer selection**: once a stale tip needs no extra full-relay connection, bitcoin-rs retires the newest automatic full-relay outbound connection that sits one beyond the slots and is older than `MINIMUM_CONNECT_TIME` (`retire_extra_full_relay_connection`, `crates/p2p/src/service.rs:976`). An operator-pinned connection is outside the count and the victim set both, as in Core: neither `IsFullOutboundConn()` nor `IsBlockOnlyConn()` includes `ConnectionType::MANUAL` (`net_processing.cpp:5558-5604`). Core's `EvictExtraOutboundPeers` (`net_processing.cpp:5604-5668`) instead retires the connection that announced a block longest ago, breaking a tie by dropping the most recently connected one. The retired count is the same; the retired connection is not. A pinned outbound peer therefore neither creates an excess nor stands as a victim, matching `IsFullOutboundConn`/`IsBlockOnlyConn` excluding `ConnectionType::MANUAL` (`net_processing.cpp:5558-5604`).
11. **Unanswered ping**: Core sends one ping per `PING_INTERVAL` and remembers the nonce it asked for; when the pong has not arrived by `TIMEOUT_INTERVAL` after that ping, `MaybeSendPing` ends the connection regardless of any other traffic (`net_processing.cpp:5698-5712`). bitcoin-rs probes on the same cadence and ends a connection when either direction has been silent for `TIMEOUT_INTERVAL`, but it credits any inbound message as receive activity and keeps no outstanding-ping record, so a peer that never answers a probe while other traffic continues is not retired by that rule here.

12. **Inbound admission**: bitcoin-rs refuses an inbound socket once the live inbound count reaches `max_peer_connections - outbound_full_relay_slots - outbound_block_relay_slots` (default `200 - 8 - 2 = 190`, `net.h:1124-1127`), closing the stream before a handshake lease exists (`crates/p2p/src/listener.rs`, `PeerTable::try_register_inbound`). Core derives the same remainder and then scores an eviction (`AttemptToEvictConnection`, `net.cpp:1695-1735`) to make room. The eviction scoring is deliberately not implemented: no bitcoin-rs sync path depends on being able to displace an inbound peer, the resource-exhaustion defect closes at the admission boundary, and adding a second peer-selection policy would need an acceptance requirement it does not have. The operator-visible consequence is that the 191st inbound connection is refused rather than replacing a chosen peer.
Expand Down
16 changes: 16 additions & 0 deletions e2e/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,3 +517,19 @@ pub fn wait_for(dur: Duration, check: &mut dyn FnMut() -> bool) -> bool {
pub fn node_stderr(node: &ProcessNode) -> String {
std::fs::read_to_string(node.evidence.join("stderr.log")).unwrap_or_default()
}

/// Asserts the node's stderr shows no panic and no `PrevHashMismatch` —
/// `context` names where a mismatch would indicate commit churn.
pub fn assert_clean_stderr(node: &ProcessNode, context: &str) {
let stderr = node_stderr(node);
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
assert_eq!(
stderr.matches("panic").count(),
0,
"node stderr contains a panic"
);
assert_eq!(
stderr.matches("PrevHashMismatch").count(),
0,
"node stderr shows PrevHashMismatch: {context}"
);
}
Loading
Loading