From c4688c8ea7f70a2f6ab129f8212f93faf931b5c8 Mon Sep 17 00:00:00 2001 From: "rearden-grok[bot]" <317016512+rearden-grok[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:01:40 -0700 Subject: [PATCH 1/4] node: ADD_ONION for Esplora when --tor-control is set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reuse the persistent key helper as onion/esplora.priv. --esplora-onion=0 skips. REST and /ws share the TCP port; log http://….onion:port. Co-authored-by: Cursor --- crates/rbitcoin-node/src/cli.rs | 9 ++++- crates/rbitcoin-node/src/config.rs | 7 ++++ crates/rbitcoin-node/src/run.rs | 12 ++++++ crates/rbitcoin-node/src/tor_control.rs | 52 ++++++++++++++++++++++++- 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/crates/rbitcoin-node/src/cli.rs b/crates/rbitcoin-node/src/cli.rs index 3d5f414b1..e05ad82c6 100644 --- a/crates/rbitcoin-node/src/cli.rs +++ b/crates/rbitcoin-node/src/cli.rs @@ -296,7 +296,7 @@ fn operator_usage() -> String { [--listen ADDR] [--no-listen] [--connect ADDR]... [--seed-node HOST]... [--proxy HOST:PORT] [--onion HOST:PORT] [--proxy-randomize[=0|1]] [--only-net NET]... \\\n\ [--tor-control [HOST:PORT]] [--tor-control-cookie PATH] [--tor-control-password PASS] \\\n\ [--i2p-sam [HOST:PORT]] [--i2p-accept-incoming] \\\n\ - [--electrum-listen ADDR] [--esplora-listen ADDR] \\\n\ + [--electrum-listen ADDR] [--esplora-listen ADDR] [--esplora-onion[=0|1]] \\\n\ [--sh-index] [--sp-tweaks] [--sp-tweaks-dust SATS] [--max-sh-creates N] [--esplora-block-template] \\\n\ [--rpc] [--rpc-listen [ADDR]] [--rpc-token-file PATH] [--rpc-work-queue N] \\\n\ [--milestone HEIGHT] \\\n\ @@ -335,6 +335,7 @@ Scripthash: --sh-index (default off) builds Class B for Electrum/Esplora address Electrum/Esplora start without it; scripthash/address methods fail closed.\n\ --max-sh-creates N refuses Electrum/Esplora joins with more than N creates (0 = unlimited).\n\ --esplora-block-template enables GET /block-template (GBT template JSON; default off).\n\ + --esplora-onion (default on) ADD_ONION for --esplora-listen when --tor-control is set.\n\ Silent payments: --sp-tweaks (default off) writes/serves the thin BIP-352 tweak index.\n\ --sp-tweaks-dust SATS omits served P2TR outs with value <= SATS (default 1000; 0 = all; 546 = Cake electrs).\n\ RPC: --rpc unix socket {{datadir}}/rpc.sock; --rpc-listen [ADDR] adds TCP (default 127.0.0.1 and Core-matching port). Token {{datadir}}/rpc.token (Bearer). No --rpcuser.\n\ @@ -376,6 +377,7 @@ fn is_bool_key(key: &str) -> bool { "sh_index" | "sp_tweaks" | "esplora_block_template" + | "esplora_onion" | "blocks_only" | "prefill_compact" | "persist_mempool" @@ -574,6 +576,8 @@ mod tests { "--sh-index", "--sp-tweaks", "--sp-tweaks-dust", + "--esplora-block-template", + "--esplora-onion", "--rpc", "--rpc-listen", "--rpc-token-file", @@ -1136,6 +1140,9 @@ mod tests { assert!(gbt_eq.esplora_block_template); let off = ready_config(["rbitcoin-node", "--esplora-block-template=0"]); assert!(!off.esplora_block_template); + assert!(NodeConfig::default().esplora_onion); + let onion_off = ready_config(["rbitcoin-node", "--esplora-onion=0"]); + assert!(!onion_off.esplora_onion); } #[test] diff --git a/crates/rbitcoin-node/src/config.rs b/crates/rbitcoin-node/src/config.rs index e139ac0e1..105797e69 100644 --- a/crates/rbitcoin-node/src/config.rs +++ b/crates/rbitcoin-node/src/config.rs @@ -251,6 +251,8 @@ pub struct NodeConfig { pub max_sh_creates: u32, /// Opt-in Esplora `GET /block-template` (GBT template JSON). Default off. pub esplora_block_template: bool, + /// ADD_ONION for `--esplora-listen` when `--tor-control` is set. Default on. + pub esplora_onion: bool, /// Skip script/prevout checks for blocks at or below this height (0 = off). pub milestone_height: u32, /// Set when conf or CLI applied `milestone` (including 0). @@ -323,6 +325,7 @@ impl Default for NodeConfig { sptweaks_dust: rbitcoin_electrum::DEFAULT_TWEAKS_MIN_DUST, max_sh_creates: 0, esplora_block_template: false, + esplora_onion: true, milestone_height: 0, milestone_explicit: false, inhibit_suspend: false, @@ -839,6 +842,10 @@ impl NodeConfig { self.esplora_block_template = parse_conf_bool(val) .map_err(|e| NodeError::Config(format!("conf esplora_block_template: {e}")))?; } + "esplora_onion" => { + self.esplora_onion = parse_conf_bool(val) + .map_err(|e| NodeError::Config(format!("conf esplora_onion: {e}")))?; + } "rpc" => { self.rpc.socket = parse_conf_bool(val) .map_err(|e| NodeError::Config(format!("conf rpc: {e}")))?; diff --git a/crates/rbitcoin-node/src/run.rs b/crates/rbitcoin-node/src/run.rs index 8e8ab9248..e2c120a28 100644 --- a/crates/rbitcoin-node/src/run.rs +++ b/crates/rbitcoin-node/src/run.rs @@ -673,6 +673,18 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { &mempool, ) .await; + 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) + .await?; + info!( + "esplora onion http://{}.onion:{} (/ws same port)", + hs.service_id, + h.local_addr.port() + ); + } + } let mut rpc_handle: Option = None; if (config.rpc.socket || config.rpc.listen.is_some()) && !shutdown.requested() { diff --git a/crates/rbitcoin-node/src/tor_control.rs b/crates/rbitcoin-node/src/tor_control.rs index 7c500354a..c8279b600 100644 --- a/crates/rbitcoin-node/src/tor_control.rs +++ b/crates/rbitcoin-node/src/tor_control.rs @@ -188,16 +188,33 @@ impl TorControl { Ok(hs) } - pub async fn add_electrum_onion( + pub async fn add_named_onion( &mut self, datadir: &Path, + name: &str, bound: SocketAddr, ) -> Result { - let key_path = datadir.join("onion").join("electrum.priv"); + let key_path = datadir.join("onion").join(format!("{name}.priv")); let virt = bound.port(); let target = SocketAddr::from(([127, 0, 0, 1], virt)); self.add_onion_persistent(&key_path, virt, target).await } + + pub async fn add_electrum_onion( + &mut self, + datadir: &Path, + bound: SocketAddr, + ) -> Result { + self.add_named_onion(datadir, "electrum", bound).await + } + + pub async fn add_esplora_onion( + &mut self, + datadir: &Path, + bound: SocketAddr, + ) -> Result { + self.add_named_onion(datadir, "esplora", bound).await + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -766,4 +783,35 @@ mod tests { let _ = std::fs::remove_file(&cookie_path); let _ = std::fs::remove_dir_all(&dir); } + + #[tokio::test] + async fn esplora_hidden_service_add_onion() { + let cookie = vec![0x77, 0x88]; + let (addr, log) = fake_control(Some(cookie.clone()), None).await; + let cookie_path = tmp_cookie(&cookie); + let mut ctl = TorControl::connect_and_auth(addr, TorAuth::Cookie(cookie_path.clone())) + .await + .unwrap(); + let dir = std::env::temp_dir().join(format!( + "rbtc-tor-esplora-hs-{}-{}", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let bound: SocketAddr = "127.0.0.1:3000".parse().unwrap(); + let hs = ctl.add_esplora_onion(&dir, bound).await.unwrap(); + assert_eq!(hs.service_id, FAKE_SID); + let cmds = log.lock().unwrap().clone(); + let onion = cmds + .iter() + .find(|c| c.starts_with("ADD_ONION ")) + .cloned() + .expect("ADD_ONION"); + assert!(onion.contains("Port=3000,127.0.0.1:3000"), "{onion}"); + assert!(std::path::Path::new(&dir.join("onion").join("esplora.priv")).is_file()); + let _ = std::fs::remove_file(&cookie_path); + let _ = std::fs::remove_dir_all(&dir); + } } From 8d56ca515624fccdba86f2d202e68395615de59a Mon Sep 17 00:00:00 2001 From: "rearden-grok[bot]" <317016512+rearden-grok[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:32:50 -0700 Subject: [PATCH 2/4] rpc: getnetworkinfo.localaddresses lists wallet onions Electrum and Esplora hidden-service hostnames are operator rows, not discover IPs. They stay visible with --no-discover. Co-authored-by: Cursor --- crates/rbitcoin-net/src/peers.rs | 29 ++++++++++++++++++++---- crates/rbitcoin-node/src/run.rs | 4 ++++ crates/rbitcoin-rpc/src/methods_tests.rs | 28 +++++++++++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/crates/rbitcoin-net/src/peers.rs b/crates/rbitcoin-net/src/peers.rs index cd4269d9f..8066b23ad 100644 --- a/crates/rbitcoin-net/src/peers.rs +++ b/crates/rbitcoin-net/src/peers.rs @@ -1105,6 +1105,7 @@ pub struct PeerHub { peer_timeout_secs: AtomicU64, /// Addresses we advertise (`getnetworkinfo.localaddresses`). external_ips: Mutex>, + wallet_onions: Mutex>, /// P2P listen port used with advertised external IPs. listen_port: AtomicU16, /// Core `-discover`. Off: never self-announce, even with `--external-ip`. @@ -1182,6 +1183,7 @@ impl PeerHub { addr_response_cache: Mutex::new(HashMap::new()), peer_timeout_secs: AtomicU64::new(60), external_ips: Mutex::new(Vec::new()), + wallet_onions: Mutex::new(Vec::new()), listen_port: AtomicU16::new(0), discover: AtomicBool::new(true), asmap: Mutex::new(None), @@ -1239,6 +1241,13 @@ impl PeerHub { *self.external_ips.lock().unwrap_or_else(|e| e.into_inner()) = ips; } + pub fn set_wallet_onion(&self, host: String, port: u16) { + self.wallet_onions + .lock() + .unwrap_or_else(|e| e.into_inner()) + .push((host, port)); + } + pub fn set_listen_port(&self, port: u16) { self.listen_port.store(port, Ordering::Relaxed); } @@ -1250,21 +1259,31 @@ impl PeerHub { /// `getnetworkinfo.localaddresses` rows for operator-advertised IPs. pub fn rpc_local_addresses(&self) -> Vec<(String, u16, i32)> { const LOCAL_MANUAL: i32 = 4; + let mut rows: Vec<(String, u16, i32)> = self + .wallet_onions + .lock() + .unwrap_or_else(|e| e.into_inner()) + .iter() + .cloned() + .map(|(address, port)| (address, port, LOCAL_MANUAL)) + .collect(); if !self.discover.load(Ordering::Relaxed) { - return Vec::new(); + return rows; } let port = self.listen_port.load(Ordering::Relaxed); if port == 0 { - return Vec::new(); + return rows; } let ips = self .external_ips .lock() .unwrap_or_else(|e| e.into_inner()) .clone(); - ips.into_iter() - .map(|ip| (ip.to_string(), port, LOCAL_MANUAL)) - .collect() + rows.extend( + ips.into_iter() + .map(|ip| (ip.to_string(), port, LOCAL_MANUAL)), + ); + rows } pub fn advertise_local_socket(&self) -> Option { diff --git a/crates/rbitcoin-node/src/run.rs b/crates/rbitcoin-node/src/run.rs index e2c120a28..6a1db3a65 100644 --- a/crates/rbitcoin-node/src/run.rs +++ b/crates/rbitcoin-node/src/run.rs @@ -662,6 +662,8 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { h.local_addr.port() ); let _ = electrum_onion.set((format!("{}.onion", hs.service_id), h.local_addr.port())); + node.peers + .set_wallet_onion(format!("{}.onion", hs.service_id), h.local_addr.port()); } let (esplora_handles, esplora_tip_bridge) = start_esplora_if_ready( sh_tip_ready, @@ -683,6 +685,8 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { hs.service_id, h.local_addr.port() ); + node.peers + .set_wallet_onion(format!("{}.onion", hs.service_id), h.local_addr.port()); } } diff --git a/crates/rbitcoin-rpc/src/methods_tests.rs b/crates/rbitcoin-rpc/src/methods_tests.rs index 7a0abaa67..8daa91177 100644 --- a/crates/rbitcoin-rpc/src/methods_tests.rs +++ b/crates/rbitcoin-rpc/src/methods_tests.rs @@ -4222,6 +4222,34 @@ fn getnetworkinfo_localaddresses_from_externalip() { let _ = std::fs::remove_dir_all(&dir); } +#[test] +fn getnetworkinfo_includes_electrum_onion() { + use rbitcoin_net::PeerHub; + + let (mut ctx, dir) = ctx_empty(); + let hub = PeerHub::new(); + hub.set_discover(false); + hub.set_wallet_onion( + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion".into(), + 50001, + ); + hub.set_wallet_onion( + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabce.onion".into(), + 3000, + ); + ctx.peers = Some(hub); + let info = dispatch(&ctx, "getnetworkinfo", vec![]).unwrap(); + let addrs = info["localaddresses"].as_array().expect("array"); + assert_eq!(addrs.len(), 2, "{info}"); + assert_eq!( + addrs[0]["address"], + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion" + ); + assert_eq!(addrs[0]["port"], 50001); + assert_eq!(addrs[1]["port"], 3000); + let _ = std::fs::remove_dir_all(&dir); +} + #[test] fn getpeerinfo_lists_registered_session() { use bitcoin::p2p::address::Address; From 4101b002193bcf5db5fea747d36269f75453e987 Mon Sep 17 00:00:00 2001 From: "rearden-grok[bot]" <317016512+rearden-grok[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:42:56 -0700 Subject: [PATCH 3/4] node: STREAM FORWARD Electrum and Esplora when I2P incoming is on Reuse the SAM persist helper with {datadir}/i2p/electrum.priv and esplora.priv. Separate sessions from the P2P destination. Co-authored-by: Cursor --- crates/rbitcoin-node/src/run.rs | 120 ++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/crates/rbitcoin-node/src/run.rs b/crates/rbitcoin-node/src/run.rs index 6a1db3a65..3eb512c8a 100644 --- a/crates/rbitcoin-node/src/run.rs +++ b/crates/rbitcoin-node/src/run.rs @@ -689,6 +689,34 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { .set_wallet_onion(format!("{}.onion", hs.service_id), h.local_addr.port()); } } + let mut i2p_wallet = Vec::new(); + if config.listen.i2p_accept_incoming { + if let Some(addr) = config.listen.i2p_sam { + if let Some(h) = electrum_handles.first() { + i2p_wallet.push( + start_i2p_named_forward( + addr, + config.datadir.path(), + "electrum", + h.local_addr.port(), + ) + .await?, + ); + } + if let Some(h) = esplora_handles.first() { + i2p_wallet.push( + start_i2p_named_forward( + addr, + config.datadir.path(), + "esplora", + h.local_addr.port(), + ) + .await?, + ); + } + } + } + let _i2p_wallet = i2p_wallet; let mut rpc_handle: Option = None; if (config.rpc.socket || config.rpc.listen.is_some()) && !shutdown.requested() { @@ -1352,6 +1380,23 @@ fn electrum_tip_notify(ev: TipEvent) -> Option { }) } +async fn start_i2p_named_forward( + sam_addr: SocketAddr, + datadir: &Path, + name: &str, + port: u16, +) -> Result { + let dest = datadir.join("i2p").join(format!("{name}.priv")); + let mut sam = rbitcoin_net::I2pSam::connect_persistent(sam_addr, &dest) + .await + .map_err(|e| NodeError::Init(format!("i2p {name} session: {e}")))?; + sam.stream_forward(port) + .await + .map_err(|e| NodeError::Init(format!("i2p {name} STREAM FORWARD {port}: {e}")))?; + info!("i2p {name} STREAM FORWARD to 127.0.0.1:{port}"); + Ok(sam) +} + async fn start_electrum_if_ready( sh_tip_ready: bool, addr: Option, @@ -2631,4 +2676,79 @@ mod tests { drop(held); let _ = std::fs::remove_dir_all(&dir); } + + #[tokio::test] + async fn electrum_i2p_forward_when_sam_incoming() { + use std::sync::{Arc, Mutex}; + use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; + use tokio::net::{TcpListener, TcpStream}; + + async fn write_line(s: &mut TcpStream, line: &str) { + s.write_all(line.as_bytes()).await.unwrap(); + s.write_all(b"\n").await.unwrap(); + s.flush().await.unwrap(); + } + async fn read_line(s: &mut TcpStream) -> Option { + let mut reader = BufReader::new(s); + let mut line = String::new(); + let n = reader.read_line(&mut line).await.ok()?; + if n == 0 { + return None; + } + Some(line.trim_end_matches(['\r', '\n']).to_string()) + } + + let log = Arc::new(Mutex::new(Vec::new())); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let log_acc = Arc::clone(&log); + tokio::spawn(async move { + loop { + let Ok((mut s, _)) = listener.accept().await else { + break; + }; + let log = Arc::clone(&log_acc); + tokio::spawn(async move { + loop { + let Some(line) = read_line(&mut s).await else { + break; + }; + let up = line.to_ascii_uppercase(); + if up.starts_with("HELLO VERSION") { + write_line(&mut s, "HELLO REPLY RESULT=OK VERSION=3.1").await; + } else if up.starts_with("SESSION CREATE") { + write_line(&mut s, "SESSION STATUS RESULT=OK DESTINATION=walletfake") + .await; + } else if up.starts_with("STREAM FORWARD") { + log.lock().unwrap().push(line); + write_line(&mut s, "STREAM STATUS RESULT=OK").await; + } + } + }); + } + }); + + let dir = std::env::temp_dir().join(format!( + "rbtc-i2p-wallet-{}-{}", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let sam = start_i2p_named_forward(addr, &dir, "electrum", 50001) + .await + .unwrap(); + drop(sam); + assert_eq!( + std::fs::read_to_string(dir.join("i2p").join("electrum.priv")) + .unwrap() + .trim(), + "walletfake" + ); + let fw = log.lock().unwrap().clone(); + assert_eq!(fw.len(), 1, "{fw:?}"); + assert!(fw[0].contains("PORT=50001"), "{}", fw[0]); + let _ = std::fs::remove_dir_all(&dir); + } } From 48e314c0be0fb0f67d940f46e27743d388d7d1c2 Mon Sep 17 00:00:00 2001 From: "rearden-grok[bot]" <317016512+rearden-grok[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:46:45 -0700 Subject: [PATCH 4/4] docs+nix: Esplora hiddenService and wallet onion URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NixOS esplora.hiddenService implies tor.control like Electrum. OPERATOR/COMPAT: onion is plain TCP; Sparrow tcp://….onion:port. Co-authored-by: Cursor --- COMPAT.md | 5 +++-- OPERATOR.md | 11 ++++++++--- nix/modules/rbitcoin.nix | 11 +++++++++-- nix/tests/nixos-module-eval.nix | 3 +++ 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/COMPAT.md b/COMPAT.md index df2715adf..a48fb4538 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -138,7 +138,7 @@ Per-method notes, auth, and the shindex matrix live in | relayfee / estimatefee / histogram / `mempool.get_info` | done | Libre min + live median. `mempool.get_info` is 1.6 (`minrelaytxfee` replaces `relayfee` for 1.6 clients; `relayfee` stays for 1.4). | | outpoint.get_status / subscribe / unsubscribe | done | Electrum **1.7** methods; `protocol_max` stays **1.6** until `scriptpubkey.*`. Spent = confirmed-strong or mempool. | | silentpayments.subscribe / unsubscribe | done | Frigate remote-scanner: session-only scan key; historical + tip notifies via tweak index / naive `tweaks_for_height`. Not Cake `tweaks.subscribe`. | -| TLS | external | terminate at reverse proxy; node is plain TCP. In-binary 50002 + onion is parked **Q-63**. | +| TLS | external | terminate at reverse proxy for **clearnet**; onion is plain TCP (`ADD_ONION` to loopback). In-binary 50002 + TLS is parked **Q-63**. | ### Protocol versions @@ -215,7 +215,8 @@ without the dialect is an error. `server.features.protocol_max` remains ## Esplora REST surface Plain HTTP via `--esplora-listen` / conf `esplora_listen` (default **off**). TLS -via reverse proxy; app `ServeLimits` always on (same model as Electrum). +via reverse proxy for **clearnet**; onion is plain TCP (`ADD_ONION` to loopback). +App `ServeLimits` always on (same model as Electrum). | Endpoint group | Status | Notes | |----------------|--------|-------| diff --git a/OPERATOR.md b/OPERATOR.md index cd6a721b3..836cb7652 100644 --- a/OPERATOR.md +++ b/OPERATOR.md @@ -393,6 +393,7 @@ Clean smoke: | `--sp-tweaks-dust SATS` | `sp_tweaks_dust=` | **1000** — omit served P2TR outs with `value <= SATS` (`0` = serve all; **546** matches Cake electrs) | | `--electrum-listen [ADDR]` | `electrum_listen=` | disabled; omit ADDR → `127.0.0.1:50001`. Address/scripthash methods need `--sh-index` | | `--esplora-listen [ADDR\|PATH]` | `esplora_listen=` | disabled (Esplora REST); omit ADDR → `127.0.0.1:3000`; a filesystem path is unix HTTP (mode **0660**, dummy `Host: api` is fine). Address/scripthash methods need `--sh-index` | +| `--esplora-onion[=0\|1]` | `esplora_onion=` | **on** — `ADD_ONION` for Esplora when `--tor-control` is set | | `--esplora-block-template` | `esplora_block_template=` | **off** — `GET /block-template` is 404; on = GBT JSON (same as RPC template mode) | | `--rpc` | `rpc=` | **off** — unix JSON-RPC `{datadir}/rpc.sock` (mode 0600) | | `--rpc-listen [ADDR]` | `rpc_listen=` | disabled — implies `--rpc`; omit ADDR → `127.0.0.1` and Core-matching RPC port | @@ -458,9 +459,13 @@ ADDR for `127.0.0.1:9051`. Failed AUTH is a start error. Unset: no control socket. With `--electrum-listen`, the node `ADD_ONION`s that TCP port to `127.0.0.1:` and logs `….onion:port`. The private key is `{datadir}/onion/electrum.priv` (0600). `server.features.hosts` is -`{ ".onion": { "tcp_port": N } }` with no `ssl_port`. JSON-RPC stays off -the onion (`rpc.sock` / `--rpc-listen` only). Cookie path differs by distro; -pass `--tor-control-cookie` rather than globbing. +`{ ".onion": { "tcp_port": N } }` with no `ssl_port`. With +`--esplora-listen`, the same control port `ADD_ONION`s Esplora (`{datadir}/onion/esplora.priv`); +REST and `/ws` share that TCP port (`http://….onion:`). `--esplora-onion=0` +skips Esplora HS. `getnetworkinfo.localaddresses` lists those onion hostnames +even with `--no-discover`. Sparrow: `tcp://.onion:50001` (plain TCP; no +in-binary TLS). JSON-RPC stays off the onion (`rpc.sock` / `--rpc-listen` only). +Cookie path differs by distro; pass `--tor-control-cookie` rather than globbing. `--i2p-sam [HOST:PORT]` talks to **system i2pd** SAM v3 (not SOCKS, not Arti). Omit ADDR for `127.0.0.1:7656`. Failed HELLO / `SESSION CREATE` is a start diff --git a/nix/modules/rbitcoin.nix b/nix/modules/rbitcoin.nix index ac40cf019..265af5353 100644 --- a/nix/modules/rbitcoin.nix +++ b/nix/modules/rbitcoin.nix @@ -39,12 +39,12 @@ let else "${address}:${toString port}"; - needTorControl = cfg.tor.control != null || cfg.electrum.hiddenService; + needTorControl = cfg.tor.control != null || cfg.electrum.hiddenService || cfg.esplora.hiddenService; needI2pSam = cfg.i2p.sam != null; torControlAddr = if cfg.tor.control != null then cfg.tor.control - else if cfg.electrum.hiddenService then + else if cfg.electrum.hiddenService || cfg.esplora.hiddenService then "127.0.0.1:9051" else null; @@ -95,6 +95,7 @@ let ++ optional needI2pSam "--i2p-sam" ++ optional needI2pSam cfg.i2p.sam ++ optional cfg.i2p.acceptIncoming "--i2p-accept-incoming" + ++ optional cfg.esplora.hiddenService "--esplora-onion" ++ cfg.extraArgs; in { @@ -348,6 +349,12 @@ in default = false; description = "Open the Esplora listen port in the NixOS firewall."; }; + + hiddenService = mkOption { + type = types.bool; + default = false; + description = "ADD_ONION for Esplora when --esplora-listen is on. Implies tor.control 127.0.0.1:9051 if unset. REST and /ws share the TCP port."; + }; }; }; diff --git a/nix/tests/nixos-module-eval.nix b/nix/tests/nixos-module-eval.nix index 52d366c04..870de07ad 100644 --- a/nix/tests/nixos-module-eval.nix +++ b/nix/tests/nixos-module-eval.nix @@ -56,6 +56,7 @@ let esplora = { enable = true; openFirewall = true; + hiddenService = true; }; }; } @@ -106,6 +107,7 @@ assert defaultCfg.onlyNet == [ ]; assert defaultCfg.tor.control == null; assert defaultCfg.tor.controlCookie == null; assert defaultCfg.electrum.hiddenService == false; +assert defaultCfg.esplora.hiddenService == false; assert defaultCfg.i2p.sam == null; assert defaultCfg.i2p.acceptIncoming == false; assert cfg.services.rbitcoin.p2p.port == 18444; @@ -127,6 +129,7 @@ assert builtins.match ".*--listen 127.0.0.1:18444.*" execStart != null; assert builtins.match ".*--rpc-listen 127.0.0.1:18443.*" execStart != null; assert builtins.match ".*--electrum-listen 127.0.0.1:50001.*" execStart != null; assert builtins.match ".*--esplora-listen 127.0.0.1:3000.*" execStart != null; +assert builtins.match ".*--esplora-onion.*" execStart != null; assert builtins.match ".*--shindex.*" execStart != null; assert builtins.match ".*--sptweaks.*" execStart != null; assert builtins.match ".*--log-level debug.*" execStart != null;