Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
954bc22
feat(p2p): add opt-in Tor and IVNP I2P routing
gosunuts Oct 26, 2026
268be62
Merge main and preserve overlay endpoints in announcement retries
gosunuts Oct 26, 2026
506cffd
fix(ci): sync standalone lockfiles for overlay dependencies
gosunuts Oct 26, 2026
3bfc441
fix(p2p): address overlay lifecycle and policy review findings
gosunuts Oct 26, 2026
93dc702
fix(p2p): attribute routed destination failures to address retries
gosunuts Oct 26, 2026
bc1d8a8
refactor(p2p): consolidate overlay configuration and session ownership
gosunuts Oct 26, 2026
86775d4
fix(ci): use diagnostic equality assertions for empty values
gosunuts Oct 26, 2026
7bbf58d
test(p2p): align inactive admission diagnostic expectation
gosunuts Oct 26, 2026
e003ccc
test(rpc): pin getnetworkinfo networks in batch differential fixture
Oct 27, 2026
34b0ee8
fix(p2p): bind SAM streams to requested identities
gosunuts Oct 27, 2026
d97afa0
fix(p2p): harden overlay authentication and recovery
gosunuts Oct 27, 2026
a7deb03
Merge main and preserve both layered configuration test suites
AgentDawn Oct 27, 2026
9c8e93d
Synchronize standalone lockfiles with overlay dependencies
AgentDawn Oct 27, 2026
d97de4c
Account for unresolved I2P destinations when retrying peers
AgentDawn Oct 27, 2026
0c7614c
Keep the combined startup validation gate Clippy-clean
AgentDawn Oct 27, 2026
6ee8a5a
Strengthen overlay exchange and publication recovery tests
AgentDawn Oct 27, 2026
bd5ac73
refactor(addrman): reset pre-release storage to canonical v1
gosunuts Oct 27, 2026
d2ff50b
refactor(p2p): separate anonymous sessions from peer destinations
gosunuts Oct 27, 2026
259c20e
refactor(p2p): unify pinned peer connection retries
gosunuts Oct 27, 2026
4d7e5b8
fix(p2p): retire only leases belonging to the failed SAM epoch
AgentDawn Oct 27, 2026
8c8eac2
fix(p2p): observe lease epochs during payload reads
gosunuts Oct 27, 2026
c33eac6
Merge branch 'main' into feat/1601-overlay-routing
rabbitson87 Oct 28, 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
39 changes: 39 additions & 0 deletions .github/workflows/overlay-interop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: overlay-interoperability

on:
workflow_dispatch:

permissions:
contents: read

jobs:
external-backends:
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: ./.github/actions/native-deps
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
- name: Install external backends
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y tor golang-go
git clone https://github.com/gosuda/IVNP.git "$RUNNER_TEMP/ivnp"
git -C "$RUNNER_TEMP/ivnp" checkout "$(python3 scripts/overlay-interop.py --ivnp-revision)"
cd "$RUNNER_TEMP/ivnp"
# Go's versioned toolchain mechanism fetches the module's required toolchain.
GOTOOLCHAIN=go1.27.0+auto go build -o "$RUNNER_TEMP/ivnpd" ./cmd/ivnpd
- run: cargo build --locked --bin bitcoin-rs
- name: Bitcoin handshake, block exchange, inbound admission, and identity restart
run: python3 scripts/overlay-interop.py --ivnpd "$RUNNER_TEMP/ivnpd" --tor /usr/bin/tor
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: overlay-interoperability
path: |
target/overlay-interop
target/process-harness/e2e
24 changes: 24 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ secp256k1 = { version = ">=0.29.1, <0.30", default-features = false, features =
] }

# sha2 0.11: default features are alloc+oid; no std/asm feature is exposed.
base64 = "=0.22.1"
sha3 = { version = ">=0.11, <0.12", default-features = false }
sha2 = { version = ">=0.11, <0.12", default-features = false }
bitcoin_hashes = { version = ">=0.14, <0.15", default-features = false, features = [
"alloc", "std",
Expand Down
2 changes: 1 addition & 1 deletion crates/mempool/src/admission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -891,7 +891,7 @@ mod tests {

fn source() -> PeerToken {
PeerToken {
addr: core::net::SocketAddr::from(([127, 0, 0, 1], 8333)),
addr: bitcoin_rs_primitives::PeerEndpoint::from(([127, 0, 0, 1], 8333)),
connection_id: 7,
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/mempool/src/gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3718,7 +3718,7 @@ mod tests {
candidate.inputs.push(candidate.inputs[0].clone());
candidate.inputs[0].witness = Witness::from_stack(vec![vec![1]]);
let origin = AdmissionOrigin::Peer(crate::PeerToken {
addr: core::net::SocketAddr::from(([127, 0, 0, 1], 8333)),
addr: bitcoin_rs_primitives::PeerEndpoint::from(([127, 0, 0, 1], 8333)),
connection_id: 7,
});
let request = admit_request(&gateway, &candidate, origin);
Expand Down
4 changes: 2 additions & 2 deletions crates/mempool/src/mutation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ impl MutationResult {
/// Identifies the network peer a transaction arrived from.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct PeerToken {
/// The peer's wire address.
pub addr: core::net::SocketAddr,
/// The peer's logical Bitcoin endpoint, including anonymous inbound Tor handles.
pub addr: bitcoin_rs_primitives::PeerEndpoint,
Comment thread
gosunuts marked this conversation as resolved.
Outdated
/// The node-local id of the connection the peer arrived on.
pub connection_id: u64,
}
Expand Down
2 changes: 1 addition & 1 deletion crates/mempool/src/orphan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ mod tests {
};
fn source(id: u64) -> PeerToken {
PeerToken {
addr: core::net::SocketAddr::from(([127, 0, 0, 1], 8333)),
addr: bitcoin_rs_primitives::PeerEndpoint::from(([127, 0, 0, 1], 8333)),
connection_id: id,
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/node/benches/sync_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1230,7 +1230,7 @@ fn install_synthetic_peers(
.unwrap_or_else(|error| panic!("invalid synthetic peer port: {error}"));
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port);
let (outbound_tx, outbound_rx) = unbounded::<bitcoin_rs_p2p::QueuedFrame>();
peer_table.register(addr, bitcoin_rs_p2p::PeerLease::new(outbound_tx));
peer_table.register(addr.into(), bitcoin_rs_p2p::PeerLease::new(outbound_tx));
outbound_rxs.push(outbound_rx);
}
outbound_rxs
Expand Down
6 changes: 3 additions & 3 deletions crates/node/src/chain_effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -784,15 +784,15 @@ mod tests {
let available = orphan_child(OutPoint::new(parent, 0));
let missing = orphan_child(OutPoint::new(Hash256::from_le_bytes(&[43; 32]).into(), 0));
let source = PeerToken {
addr: std::net::SocketAddr::from(([127, 0, 0, 1], 18444)),
addr: std::net::SocketAddr::from(([127, 0, 0, 1], 18444)).into(),
connection_id: 7,
};
let replacement = PeerToken {
connection_id: 8,
..source
};
let missing_source = PeerToken {
addr: std::net::SocketAddr::from(([127, 0, 0, 1], 18445)),
addr: std::net::SocketAddr::from(([127, 0, 0, 1], 18445)).into(),
connection_id: 9,
};
let chain = AdmissionCoins::default();
Expand Down Expand Up @@ -883,7 +883,7 @@ mod tests {
let outpoint = OutPoint::new(parent, 0);
let child = orphan_child(outpoint);
let source = PeerToken {
addr: std::net::SocketAddr::from(([127, 0, 0, 1], 18444)),
addr: std::net::SocketAddr::from(([127, 0, 0, 1], 18444)).into(),
connection_id: 7,
};
let chain = AdmissionCoins::default();
Expand Down
25 changes: 25 additions & 0 deletions crates/node/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ pub struct StorageConfig {
/// Resolved P2P configuration.
#[derive(Clone, Debug)]
pub struct P2pConfig {
/// Opt-in Tor and IVNP I2P routing.
pub overlay: bitcoin_rs_p2p::OverlayConfig,
/// P2P message-start bytes.
pub magic: [u8; 4],
/// P2P listener bind addresses.
Expand Down Expand Up @@ -283,6 +285,24 @@ impl NodeConfig {
prune_target_mb: settings.storage.prune_target_mb.unwrap_or(0),
},
p2p: P2pConfig {
overlay: bitcoin_rs_p2p::OverlayConfig {
proxy: settings.p2p.proxy,
onion_proxy: settings.p2p.onion_proxy,
onlynet: settings.p2p.onlynet.clone().unwrap_or_default(),
randomize_credentials: settings.p2p.proxy_randomize_credentials.unwrap_or(true),
i2p_sam: settings.p2p.i2p_sam,
i2p_accept: settings.p2p.i2p_accept.unwrap_or(false),
tor_control: settings.p2p.tor_control,
tor_cookie: settings.p2p.tor_cookie.clone(),
identity_dir: PathBuf::from(format!(
"overlay-{:08x}",
u32::from_be_bytes(settings.p2p.magic.unwrap_or(profile.magic))
)),
port: settings
.p2p
.overlay_port
.unwrap_or_else(|| selection.consensus_network().default_p2p_port()),
Comment thread
gosunuts marked this conversation as resolved.
},
magic: settings.p2p.magic.unwrap_or(profile.magic),
listen: settings.p2p.listen.clone().unwrap_or(profile.listen),
dns_seeds_enabled: settings.p2p.dns_seeds.unwrap_or(profile.dns_seeds),
Expand Down Expand Up @@ -342,6 +362,10 @@ impl NodeConfig {
/// This is the fail-early gate for engine selection: an engine this build
/// cannot execute is rejected here — before the data directory opens,
/// chainstate exists, or any worker starts.
#[expect(
clippy::too_many_lines,
reason = "Keep resolved cross-field checks in one ordered fail-early gate before startup"
)]
pub fn validate(&self) -> Result<()> {
anyhow::ensure!(
self.mempool_snapshot_max_bytes >= 1024 * 1024
Expand Down Expand Up @@ -406,6 +430,7 @@ impl NodeConfig {
support is not compiled in (enable the `kernel` feature)",
self.validation.engine
);
self.p2p.overlay.validate()?;
if self.p2p.magic != self.network.magic() {
anyhow::ensure!(
self.network == Network::Mainnet,
Expand Down
70 changes: 69 additions & 1 deletion crates/node/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use bitcoin_rs_consensus::ValidationEngine;
use bitcoin_rs_storage::StorageBackend;

/// Canonical subnet type shared by every process-input surface.
pub use bitcoin_rs_p2p::IpSubnet;
pub use bitcoin_rs_p2p::{IpSubnet, PeerNetwork};

/// Parses a network selection spelling.
pub fn parse_network(value: &str) -> std::result::Result<NetworkSelection, String> {
Expand Down Expand Up @@ -103,6 +103,20 @@ pub fn parse_connect_endpoint(value: &str) -> std::result::Result<String, String
Ok(value.to_owned())
}

/// Parse one allowed remote network without DNS resolution.
pub fn parse_peer_network(value: &str) -> std::result::Result<bitcoin_rs_p2p::PeerNetwork, String> {
value.parse().map_err(str::to_owned)
}
fn parse_peer_networks(value: &str) -> Result<Vec<bitcoin_rs_p2p::PeerNetwork>> {
Comment thread
gosunuts marked this conversation as resolved.
if value.trim().is_empty() {
return Ok(Vec::new());
}
value
.split(',')
.map(|network| parse_peer_network(network.trim()).map_err(anyhow::Error::msg))
.collect()
}

/// Parses a comma-separated listener bind list.
fn parse_socket_list(value: &str) -> Result<Vec<SocketAddr>> {
value
Expand Down Expand Up @@ -557,6 +571,60 @@ macro_rules! option_rows {
env["BITCOIN_RS_CONNECT", parse_connect_list]
toml each("connect", parse_connect_endpoint)
}
/// SOCKS5 route for IP destinations; onion routing is configured separately.
proxy as proxy: Option<SocketAddr> {
cli[#[arg(long = "proxy")]]
env["BITCOIN_RS_PROXY", str::parse]
toml native("proxy")
}
/// SOCKS5 route for Tor v3 destinations, passed to the proxy without local DNS.
onion_proxy as onion_proxy: Option<SocketAddr> {
cli[#[arg(long = "onion-proxy")]]
env["BITCOIN_RS_ONION_PROXY", str::parse]
toml native("onion_proxy")
}
/// Loopback SAM bridge of an operator-managed IVNP router.
i2p_sam as i2p_sam: Option<SocketAddr> {
cli[#[arg(long = "i2p-sam")]]
env["BITCOIN_RS_I2P_SAM", str::parse]
toml native("i2p_sam")
}
/// Loopback Tor control endpoint enabling a persistent inbound onion service.
tor_control as tor_control: Option<SocketAddr> {
cli[#[arg(long = "tor-control")]]
env["BITCOIN_RS_TOR_CONTROL", str::parse]
toml native("tor_control")
}
/// Tor SAFECOOKIE file; otherwise use NULL authentication.
tor_cookie as tor_cookie: Option<PathBuf> {
cli[#[arg(long = "tor-cookie")]]
env["BITCOIN_RS_TOR_COOKIE", parse_path]
toml native("tor_cookie")
}
/// Accept inbound I2P streams using a persistent application destination.
i2p_accept as i2p_accept: Option<bool> {
cli[#[arg(long = "i2p-accept", num_args = 0..=1, default_missing_value = "true")]]
env["BITCOIN_RS_I2P_ACCEPT", parse_bool]
toml native("i2p_accept")
}
/// Require SOCKS username/password authentication with fresh credentials per connection.
proxy_randomize_credentials as proxy_randomize_credentials: Option<bool> {
cli[#[arg(long = "proxy-randomize-credentials", num_args = 0..=1, default_missing_value = "true")]]
env["BITCOIN_RS_PROXY_RANDOMIZE_CREDENTIALS", parse_bool]
toml native("proxy_randomize_credentials")
}
/// Public Bitcoin service port for inbound overlay services.
overlay_port as overlay_port: Option<u16> {
cli[#[arg(long = "overlay-port")]]
env["BITCOIN_RS_OVERLAY_PORT", str::parse]
toml native("overlay_port")
}
/// Restrict every peer connection to these remote networks.
onlynet as onlynet: Option<Vec<$crate::options::PeerNetwork>> {
cli[#[arg(long = "onlynet", value_delimiter = ',', value_parser = $crate::options::parse_peer_network)]]
env["BITCOIN_RS_ONLYNET", parse_peer_networks]
toml each("onlynet", parse_peer_network)
}
/// Whether fast sync (shallow, early fan-out over a larger
/// outbound set) is enabled.
fast_sync as fast_sync: Option<bool> {
Expand Down
5 changes: 5 additions & 0 deletions crates/node/src/state/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,11 @@ impl NodeState {
};
let p2p = Arc::new(bitcoin_rs_p2p::P2pService::new(
bitcoin_rs_p2p::P2pServiceConfig {
overlay: bitcoin_rs_p2p::OverlayConfig {
identity_dir: config.data_dir.join(&config.p2p.overlay.identity_dir),
..config.p2p.overlay.clone()
}
.into(),
listen_addrs: config.p2p.listen.clone(),
address_book_path: Some(config.data_dir.join("peers.dat")),
asmap_path: config.p2p.asmap.clone(),
Expand Down
Loading
Loading