diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee06b1a8d..bd31638b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,6 +131,10 @@ jobs: workspaces: ". -> target/dev" - name: tmpfs headroom run: df -h /dev/shm && free -m + - name: rbitcoin-node signet datadir regressions + run: | + cargo test -p rbitcoin-node custom_signet_datadir_appends_challenge -- --nocapture --test-threads=1 + cargo test -p rbitcoin-node default_signet_datadir_uses_builtin_challenge -- --nocapture --test-threads=1 - name: cargo test run: cargo test --workspace diff --git a/crates/rbitcoin-node/src/cli.rs b/crates/rbitcoin-node/src/cli.rs index 35963392c..89767996c 100644 --- a/crates/rbitcoin-node/src/cli.rs +++ b/crates/rbitcoin-node/src/cli.rs @@ -263,7 +263,7 @@ where "rbitcoin-node {} on {} datadir={}", env!("CARGO_PKG_VERSION"), handle.network_name(), - handle.config.datadir.path().display() + handle.config.datadir_path().display() ); if std::env::var_os("RBITCOIN_TEST_DROP_STORE").is_some() { let _ = std::fs::remove_dir_all(handle.config.store_path()); diff --git a/crates/rbitcoin-node/src/config.rs b/crates/rbitcoin-node/src/config.rs index 355ebee9d..862269f6e 100644 --- a/crates/rbitcoin-node/src/config.rs +++ b/crates/rbitcoin-node/src/config.rs @@ -1,5 +1,5 @@ use crate::error::NodeError; -use bitcoin::hex::FromHex; +use bitcoin::hex::{DisplayHex, FromHex}; use bitcoin::ScriptBuf; use rbitcoin_consensus::{default_milestone_anchor, ChainParams, Milestone}; use rbitcoin_esplora::EsploraListen; @@ -476,6 +476,32 @@ impl NodeConfig { self } + /// Datadir resolved for the active chain. + /// + /// Custom signet keeps its own `signet/` tree so peers and + /// store state do not collide with other signet challenges. Plain signet + /// uses the network default challenge. + pub fn datadir_path(&self) -> PathBuf { + let root = self.datadir.path(); + if self.network != Network::Signet { + return root.to_path_buf(); + } + let challenge = self.signet_challenge.as_ref().map_or_else( + || { + rbitcoin_consensus::ChainParams::signet() + .signet_challenge + .expect("default signet challenge") + }, + Clone::clone, + ); + let challenge = challenge.as_bytes().to_lower_hex_string(); + let custom = Path::new("signet").join(&challenge); + if root.ends_with(&custom) { + return root.to_path_buf(); + } + root.join("signet").join(challenge) + } + pub fn with_p2p_listen(mut self, addr: SocketAddr) -> Self { self.listen.p2p = P2pListen::Socket(addr); self @@ -488,7 +514,7 @@ impl NodeConfig { } pub fn store_path(&self) -> PathBuf { - self.datadir.path().join("store") + self.datadir_path().join("store") } /// Cold store directory (`{datadir-cold}/store`) when `--datadir-cold` is set. @@ -509,7 +535,7 @@ impl NodeConfig { /// Durable mempool directory (`{datadir}/mempool/`). pub fn mempool_path(&self) -> PathBuf { - self.datadir.path().join("mempool") + self.datadir_path().join("mempool") } pub fn milestone(&self) -> Milestone { @@ -758,7 +784,7 @@ impl NodeConfig { self.rpc .token_file .clone() - .unwrap_or_else(|| rbitcoin_rpc::default_token_path(self.datadir.path())) + .unwrap_or_else(|| rbitcoin_rpc::default_token_path(&self.datadir_path())) } /// Configured Core cookie file for TCP HTTP Basic authentication. @@ -771,7 +797,7 @@ impl NodeConfig { self.rpc .socket_path .clone() - .unwrap_or_else(|| rbitcoin_rpc::default_socket_path(self.datadir.path())) + .unwrap_or_else(|| rbitcoin_rpc::default_socket_path(&self.datadir_path())) } /// Owner-only mode for a directory this process just created. @@ -796,14 +822,14 @@ impl NodeConfig { /// Create `{datadir}` and standard subdirs (`store`, `mempool`) if missing. pub fn ensure_datadir(&self) -> Result<(), NodeError> { self.validate()?; - let root = self.datadir.path(); + let root = self.datadir_path(); let created_root = !root.exists(); - std::fs::create_dir_all(root).map_err(|source| NodeError::Datadir { + std::fs::create_dir_all(&root).map_err(|source| NodeError::Datadir { path: self.datadir.path.clone(), source, })?; if created_root { - Self::restrict_new_dir(root)?; + Self::restrict_new_dir(&root)?; } if root.exists() && !root.is_dir() { return Err(NodeError::Config(format!( @@ -840,7 +866,7 @@ impl NodeConfig { } } if created_root { - rbitcoin_log::info!("node: created datadir {}", self.datadir.path().display()); + rbitcoin_log::info!("node: created datadir {}", self.datadir_path().display()); } Ok(()) } diff --git a/crates/rbitcoin-node/src/lock.rs b/crates/rbitcoin-node/src/lock.rs index bef1a12f1..29a37d45d 100644 --- a/crates/rbitcoin-node/src/lock.rs +++ b/crates/rbitcoin-node/src/lock.rs @@ -23,7 +23,7 @@ pub fn lock_busy_msg(dir: &Path) -> String { /// Exclusive-lock the process datadir. pub fn lock_node_dirs(config: &NodeConfig) -> Result { Ok(DirLocks { - _files: vec![lock_dir(config.datadir.path())?], + _files: vec![lock_dir(&config.datadir_path())?], }) } diff --git a/crates/rbitcoin-node/src/run.rs b/crates/rbitcoin-node/src/run.rs index 600d56454..add2e8cde 100644 --- a/crates/rbitcoin-node/src/run.rs +++ b/crates/rbitcoin-node/src/run.rs @@ -254,7 +254,7 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { "rbitcoin-node starting version={} network={} datadir={}{} tip={start_tip} io={}", env!("CARGO_PKG_VERSION"), config.network.as_str(), - config.datadir.path().display(), + config.datadir_path().display(), config .datadir .cold @@ -462,7 +462,7 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { .expect("validate requires --tor-control with --listen-onion"); let virt = config.network.default_p2p_port(); let hs = ctl - .add_p2p_onion(config.datadir.path(), node.local_addr, virt) + .add_p2p_onion(&config.datadir_path(), node.local_addr, virt) .await?; info!("p2p onion {}.onion:{}", hs.service_id, virt); node.peers @@ -473,7 +473,7 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { } let mut i2p_sam = if let Some(addr) = config.listen.i2p_sam { let s = if config.listen.i2p_accept_incoming { - let dest = config.datadir.path().join("i2p").join("p2p.priv"); + let dest = config.datadir_path().join("i2p").join("p2p.priv"); rbitcoin_net::I2pSam::connect_persistent(addr, &dest).await } else { rbitcoin_net::I2pSam::connect(addr).await @@ -516,7 +516,7 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { } else { None }; - let peers_path = config.datadir.path().join("peers"); + let peers_path = config.datadir_path().join("peers"); let mut addrman = match AddrMan::load(&peers_path) { Ok(am) => { if !am.is_empty() { @@ -536,7 +536,7 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { AddrMan::new() } }; - let asmap = load_asmap(config.datadir.path(), config.asmap.as_deref()); + let asmap = load_asmap(&config.datadir_path(), config.asmap.as_deref()); addrman.set_asmap(asmap.clone()); addrman.set_only_net(config.listen.only_net.clone()); addrman.set_cjdns_reachable(config.listen.cjdns_reachable); @@ -574,7 +574,7 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { && !config.listen.has_pinned_connect() && addrman.is_empty() { - warn!("custom signet has no peers; use --connect ADDR or reuse a datadir with known peers"); + info!("custom signet has no peers yet; keep listening or add --connect ADDR"); } let shared_peers = std::sync::Arc::new(std::sync::Mutex::new(addrman.clone())); node.peers.set_addrman(std::sync::Arc::clone(&shared_peers)); @@ -808,7 +808,7 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { .await; if let (Some(ctl), Some(h)) = (tor_ctl.as_mut(), electrum_handles.first()) { let hs = ctl - .add_electrum_onion(config.datadir.path(), h.local_addr) + .add_electrum_onion(&config.datadir_path(), h.local_addr) .await?; info!( "electrum onion {}.onion:{}", @@ -832,7 +832,7 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { if config.esplora_onion { if let (Some(ctl), Some(h)) = (tor_ctl.as_mut(), esplora_handles.first()) { let hs = ctl - .add_esplora_onion(config.datadir.path(), h.local_addr) + .add_esplora_onion(&config.datadir_path(), h.local_addr) .await?; info!( "esplora onion http://{}.onion:{} (/ws same port)", @@ -851,7 +851,7 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { i2p_wallet.push( start_i2p_named_forward( addr, - config.datadir.path(), + &config.datadir_path(), "electrum", h.local_addr.port(), ) @@ -862,7 +862,7 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { i2p_wallet.push( start_i2p_named_forward( addr, - config.datadir.path(), + &config.datadir_path(), "esplora", h.local_addr.port(), ) @@ -938,7 +938,10 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { } } - if tip_follow_ready && config.max_run_secs != Some(0) && !shutdown.requested() { + if (tip_follow_ready || config.network == Network::Signet) + && config.max_run_secs != Some(0) + && !shutdown.requested() + { let unbound = [ ( "rpc", @@ -1017,7 +1020,7 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { .await; if let (Some(ctl), Some(h)) = (tor_ctl.as_mut(), handles.first()) { let hs = ctl - .add_electrum_onion(config.datadir.path(), h.local_addr) + .add_electrum_onion(&config.datadir_path(), h.local_addr) .await?; info!( "electrum onion {}.onion:{}", @@ -1037,7 +1040,7 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { i2p_wallet.push( start_i2p_named_forward( addr, - config.datadir.path(), + &config.datadir_path(), "electrum", h.local_addr.port(), ) @@ -1063,7 +1066,7 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { if config.esplora_onion { if let (Some(ctl), Some(h)) = (tor_ctl.as_mut(), handles.first()) { let hs = ctl - .add_esplora_onion(config.datadir.path(), h.local_addr) + .add_esplora_onion(&config.datadir_path(), h.local_addr) .await?; info!( "esplora onion http://{}.onion:{} (/ws same port)", @@ -1082,7 +1085,7 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { i2p_wallet.push( start_i2p_named_forward( addr, - config.datadir.path(), + &config.datadir_path(), "esplora", h.local_addr.port(), ) @@ -2253,6 +2256,7 @@ fn resolve_seednode(raw: &str, network: Network) -> Result { #[cfg(test)] mod tests { use super::*; + use bitcoin::hex::DisplayHex; use std::time::{SystemTime, UNIX_EPOCH}; fn tiny_regtest(dir: impl AsRef) -> NodeConfig { NodeConfig::default() @@ -2701,6 +2705,44 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); } + #[test] + fn custom_signet_datadir_appends_challenge() { + let mut cfg = NodeConfig::default() + .with_datadir("/tmp/rbitcoin-custom-signet") + .with_network(rbitcoin_primitives::Network::Signet); + cfg.signet_challenge = Some(bitcoin::ScriptBuf::from_bytes(vec![0x51])); + assert_eq!( + cfg.datadir_path(), + std::path::Path::new("/tmp/rbitcoin-custom-signet/signet/51") + ); + + let already = NodeConfig::default() + .with_datadir("/tmp/rbitcoin-custom-signet/signet/51") + .with_network(rbitcoin_primitives::Network::Signet); + let mut already = already; + already.signet_challenge = Some(bitcoin::ScriptBuf::from_bytes(vec![0x51])); + assert_eq!( + already.datadir_path(), + std::path::Path::new("/tmp/rbitcoin-custom-signet/signet/51") + ); + } + + #[test] + fn default_signet_datadir_uses_builtin_challenge() { + let cfg = NodeConfig::default() + .with_datadir("/tmp/rbitcoin-default-signet") + .with_network(rbitcoin_primitives::Network::Signet); + let challenge = rbitcoin_consensus::ChainParams::signet() + .signet_challenge + .unwrap() + .as_bytes() + .to_lower_hex_string(); + assert_eq!( + cfg.datadir_path(), + std::path::Path::new(&format!("/tmp/rbitcoin-default-signet/signet/{challenge}")) + ); + } + #[tokio::test] async fn cancelled_completes_after_request() { let sd = Shutdown::new();