diff --git a/src/config.rs b/src/config.rs index 91491aee8..ef4cc8b0a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -124,7 +124,6 @@ pub struct Config { #[cfg(feature = "liquid")] pub asset_db_path: Option, - #[cfg(feature = "electrum-discovery")] pub electrum_public_hosts: Option, #[cfg(feature = "electrum-discovery")] pub electrum_announce: bool, @@ -706,6 +705,8 @@ impl Config { let electrum_public_hosts = m .value_of("electrum_public_hosts") .map(|s| serde_json::from_str(s).expect("invalid --electrum-public-hosts")); + #[cfg(not(feature = "electrum-discovery"))] + let electrum_public_hosts: Option = None; let mut log = stderrlog::new(); // Base verbosity is 2 (Info), each -v flag adds one level: @@ -789,7 +790,6 @@ impl Config { #[cfg(feature = "liquid")] asset_db_path, - #[cfg(feature = "electrum-discovery")] electrum_public_hosts, #[cfg(feature = "electrum-discovery")] electrum_announce: m.is_present("electrum_announce"), diff --git a/src/electrum/server.rs b/src/electrum/server.rs index 7115015e4..20c1aec37 100644 --- a/src/electrum/server.rs +++ b/src/electrum/server.rs @@ -35,8 +35,9 @@ use crate::util::{create_socket, spawn_thread, BlockId, BoolThen, Channel, FullH const ELECTRS_VERSION: &str = env!("CARGO_PKG_VERSION"); const PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(1, 4); const MAX_HEADERS: usize = 2016; +use crate::electrum::ServerFeatures; #[cfg(feature = "electrum-discovery")] -use crate::electrum::{DiscoveryManager, ServerFeatures}; +use crate::electrum::DiscoveryManager; fn invalid_params(msg: impl Into) -> Error { ErrorKind::InvalidParams(msg.into()).into() @@ -153,6 +154,7 @@ struct Connection { write_timeout: Option, response_budget: Arc, checkpoint_proof_concurrency_limit: usize, + server_features: Arc, #[cfg(feature = "electrum-discovery")] discovery: Option>, rpc_logging: RpcLogging, @@ -180,6 +182,7 @@ impl Connection { write_timeout: Option, response_budget: Arc, checkpoint_proof_concurrency_limit: usize, + server_features: Arc, #[cfg(feature = "electrum-discovery")] discovery: Option>, rpc_logging: RpcLogging, salt: String, @@ -199,6 +202,7 @@ impl Connection { write_timeout, response_budget, checkpoint_proof_concurrency_limit, + server_features, #[cfg(feature = "electrum-discovery")] discovery, rpc_logging, @@ -225,13 +229,8 @@ impl Connection { Ok(json!(self.query.config().electrum_banner.clone())) } - #[cfg(feature = "electrum-discovery")] fn server_features(&self) -> Result { - let discovery = self - .discovery - .as_ref() - .chain_err(|| "discovery is disabled")?; - Ok(json!(discovery.our_features())) + Ok(json!(self.server_features.as_ref())) } fn server_donation_address(&self) -> Result { @@ -616,9 +615,8 @@ impl Connection { "server.peers.subscribe" => self.server_peers_subscribe(), "server.ping" => Ok(Value::Null), "server.version" => self.server_version(), - - #[cfg(feature = "electrum-discovery")] "server.features" => self.server_features(), + #[cfg(feature = "electrum-discovery")] "server.add_peer" => self.server_add_peer(¶ms), @@ -1249,22 +1247,34 @@ impl RPC { let notification = Channel::unbounded(); - // Discovery is enabled when electrum-public-hosts is set - #[cfg(feature = "electrum-discovery")] - let discovery = config.electrum_public_hosts.clone().map(|hosts| { + let server_features = { use crate::chain::genesis_hash; - let features = ServerFeatures { - hosts, + // Prefer the genesis block hash actually indexed at height 0 over the + // hardcoded genesis_hash(): on Liquid testnet/regtest the latter is an + // all-zero placeholder, and Elements regtest genesis is configurable. + // `start` runs after the initial sync, so height 0 is already indexed; + // fall back to genesis_hash() only if it somehow isn't. + let genesis_hash = query + .chain() + .hash_by_height(0) + .unwrap_or_else(|| genesis_hash(config.network_type)); + Arc::new(ServerFeatures { + hosts: config.electrum_public_hosts.clone().unwrap_or_default(), server_version: format!("electrs-esplora {}", ELECTRS_VERSION), - genesis_hash: genesis_hash(config.network_type), + genesis_hash, protocol_min: PROTOCOL_VERSION, protocol_max: PROTOCOL_VERSION, hash_function: "sha256".into(), pruning: None, - }; + }) + }; + + // Discovery is enabled when electrum-public-hosts is set + #[cfg(feature = "electrum-discovery")] + let discovery = config.electrum_public_hosts.as_ref().map(|_hosts| { let discovery = Arc::new(DiscoveryManager::new( config.network_type, - features, + server_features.as_ref().clone(), PROTOCOL_VERSION, config.electrum_announce, config.tor_proxy, @@ -1319,6 +1329,7 @@ impl RPC { let rpc_logging = config.rpc_logging.clone(); #[cfg(feature = "electrum-discovery")] let discovery = discovery.clone(); + let server_features = Arc::clone(&server_features); let (sender, receiver) = mpsc::sync_channel(10); senders.lock().unwrap().push(sender.clone()); @@ -1340,6 +1351,7 @@ impl RPC { write_timeout, response_budget, checkpoint_proof_concurrency_limit, + server_features, #[cfg(feature = "electrum-discovery")] discovery, rpc_logging, diff --git a/src/new_index/db.rs b/src/new_index/db.rs index 9cac4e8df..7f3b41813 100644 --- a/src/new_index/db.rs +++ b/src/new_index/db.rs @@ -681,6 +681,7 @@ mod tests { history_scan_limit: 100_000, utxos_checkpoint_limit: 1_000, electrum_txs_limit: 100, + electrum_public_hosts: None, electrum_subscription_limit: 10_000, electrum_checkpoint_proof_concurrency_limit: 2, electrum_checkpoint_merkle_cache_mb: 256, diff --git a/tests/common.rs b/tests/common.rs index 3ca849501..721b7a45c 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -146,8 +146,7 @@ impl TestRunner { db_write_buffer_size_mb: 256, initial_sync_batch_size: 250, db_cache_index_filter_blocks: false, - //#[cfg(feature = "electrum-discovery")] - //electrum_public_hosts: Option, + electrum_public_hosts: None, //#[cfg(feature = "electrum-discovery")] //electrum_announce: bool, //#[cfg(feature = "electrum-discovery")] diff --git a/tests/electrum.rs b/tests/electrum.rs index da8db0188..240ed4013 100644 --- a/tests/electrum.rs +++ b/tests/electrum.rs @@ -211,6 +211,42 @@ fn test_electrum_raw() { assert_eq!(s, expected); } +#[cfg_attr(not(feature = "liquid"), test)] +#[cfg_attr(feature = "liquid", allow(dead_code))] +fn test_electrum_server_features_without_public_hosts() { + // Electrum >= 4.7.0 calls server.features on connect, so it must answer + // even when peer discovery is off (no --electrum-public-hosts). See #229. + let (_electrum_server, electrum_addr, tester) = common::init_electrum_tester().unwrap(); + + let mut stream = TcpStream::connect(electrum_addr).unwrap(); + let s = write_and_read( + &mut stream, + "{\"jsonrpc\": \"2.0\", \"method\": \"server.features\", \"params\": [], \"id\": 1}", + ); + let reply: electrumd::jsonrpc::serde_json::Value = + electrumd::jsonrpc::serde_json::from_str(&s).unwrap(); + assert!(reply.get("error").is_none(), "server.features errored: {}", s); + + let features = &reply["result"]; + assert_eq!(features["server_version"], "electrs-esplora 0.4.1"); + assert_eq!(features["hash_function"], "sha256"); + + // The genesis hash must be the actual genesis block hash indexed at height 0, + // not the hardcoded genesis_hash() (which is an all-zero placeholder on + // Liquid testnet/regtest). The Electrum protocol serializes hashes reversed. + use bitcoin::hashes::Hash; + use bitcoin::hex::DisplayHex; + let mut genesis_bytes = tester.get_block_hash(0).unwrap().to_raw_hash().to_byte_array(); + genesis_bytes.reverse(); + let expected = genesis_bytes.to_lower_hex_string(); + assert_eq!( + features["genesis_hash"].as_str(), + Some(expected.as_str()), + "genesis_hash must be the indexed height-0 block hash: {}", + s + ); +} + #[cfg_attr(not(feature = "liquid"), test)] #[cfg_attr(feature = "liquid", allow(dead_code))] fn test_electrum_jsonrpc_errors() {