Summary
server.features is currently wired through the optional peer-discovery subsystem, so it returns a JSON-RPC -32603 ("discovery is disabled") internal error whenever --electrum-public-hosts is not configured. It would be more protocol-correct (and consistent with upstream romanz/electrs) to serve server.features independently of discovery.
Current behavior
server.features is gated behind the electrum-discovery feature and reads its payload from the DiscoveryManager:
#[cfg(feature = "electrum-discovery")]
fn server_features(&self) -> Result<Value> {
let discovery = self
.discovery
.as_ref()
.chain_err(|| "discovery is disabled")?;
Ok(json!(discovery.our_features()))
}
When electrum-discovery is built but --electrum-public-hosts is unset, self.discovery is None, so a normal client calling server.features (e.g. for a genesis-hash / protocol-version compatibility check) receives:
-32603 "discovery is disabled"
-32603 is InternalError, which is misleading for what is a valid, standard request.
Why this is a problem
server.features is general server-info metadata, not a discovery operation — clients call it to check chain/protocol compatibility before anything else. Of the fields in ServerFeatures, only hosts depends on discovery config; everything else is always available:
| Field |
Source |
Needs discovery config? |
genesis_hash |
genesis_hash(network_type) |
no |
server_version |
constant |
no |
protocol_min / protocol_max |
PROTOCOL_VERSION |
no |
hash_function |
"sha256" |
no |
pruning |
None |
no |
hosts |
--electrum-public-hosts |
yes |
The Electrum protocol explicitly allows hosts to be an empty object, so server.features can return everything else with empty hosts when discovery is unconfigured. This is what upstream romanz/electrs does — it serves server.features unconditionally.
Proposed change
- Construct the
ServerFeatures payload from config (currently built inline inside the discovery .map()), defaulting hosts to empty when --electrum-public-hosts is unset.
- Have
server.features return that payload directly instead of going through self.discovery, and ungate it from #[cfg(feature = "electrum-discovery")] so it works in plain Bitcoin builds.
- Pass the same payload into
DiscoveryManager::new() so there is a single source of truth.
server.add_peer should stay tied to discovery (it genuinely needs the DiscoveryManager). As a minor follow-up, when discovery is disabled it could return -32601 (method not found) rather than -32603, since "this server does not accept peers" is closer to method-not-found than an internal error.
Summary
server.featuresis currently wired through the optional peer-discovery subsystem, so it returns a JSON-RPC-32603("discovery is disabled") internal error whenever--electrum-public-hostsis not configured. It would be more protocol-correct (and consistent with upstream romanz/electrs) to serveserver.featuresindependently of discovery.Current behavior
server.featuresis gated behind theelectrum-discoveryfeature and reads its payload from theDiscoveryManager:When
electrum-discoveryis built but--electrum-public-hostsis unset,self.discoveryisNone, so a normal client callingserver.features(e.g. for a genesis-hash / protocol-version compatibility check) receives:-32603isInternalError, which is misleading for what is a valid, standard request.Why this is a problem
server.featuresis general server-info metadata, not a discovery operation — clients call it to check chain/protocol compatibility before anything else. Of the fields inServerFeatures, onlyhostsdepends on discovery config; everything else is always available:genesis_hashgenesis_hash(network_type)server_versionprotocol_min/protocol_maxPROTOCOL_VERSIONhash_function"sha256"pruningNonehosts--electrum-public-hostsThe Electrum protocol explicitly allows
hoststo be an empty object, soserver.featurescan return everything else with empty hosts when discovery is unconfigured. This is what upstream romanz/electrs does — it servesserver.featuresunconditionally.Proposed change
ServerFeaturespayload from config (currently built inline inside the discovery.map()), defaultinghoststo empty when--electrum-public-hostsis unset.server.featuresreturn that payload directly instead of going throughself.discovery, and ungate it from#[cfg(feature = "electrum-discovery")]so it works in plain Bitcoin builds.DiscoveryManager::new()so there is a single source of truth.server.add_peershould stay tied to discovery (it genuinely needs theDiscoveryManager). As a minor follow-up, when discovery is disabled it could return-32601(method not found) rather than-32603, since "this server does not accept peers" is closer to method-not-found than an internal error.