rostrum 8.0.0

An efficient implementation of Electrum Server with token support
Documentation
use bitcoincash::network::constants::Network;
use dirs_next::home_dir;
use std::convert::TryInto;
use std::ffi::{OsStr, OsString};
use std::fmt;
use std::fs;
use std::net::SocketAddr;
use std::net::ToSocketAddrs;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;

use crate::daemon::CookieGetter;
use crate::def::ROSTRUM_VERSION;
use crate::errors::ConnectionError;
use crate::scripthash::decode_address;
use anyhow::{Context, Result};

// by default, serve on all IPv4 interfaces
const DEFAULT_BIND_ADDRESS: [u8; 4] = [0, 0, 0, 0];
const MONITOR_BIND_ADDRESS: [u8; 4] = [127, 0, 0, 1];
const DEFAULT_SERVER_ADDRESS: [u8; 4] = [127, 0, 0, 1];

mod internal {
    #![allow(unused)]
    #![allow(clippy::all)]

    include!(concat!(env!("OUT_DIR"), "/configure_me_config.rs"));
}

/// A simple error type representing invalid UTF-8 input.
pub struct InvalidUtf8(OsString);

impl fmt::Display for InvalidUtf8 {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:?} isn't a valid UTF-8 sequence", self.0)
    }
}

/// An error that might happen when resolving an address
pub enum AddressError {
    ResolvError { addr: String, err: std::io::Error },
    NoAddrError(String),
}

impl fmt::Display for AddressError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AddressError::ResolvError { addr, err } => {
                write!(f, "Failed to resolve address {}: {}", addr, err)
            }
            AddressError::NoAddrError(addr) => write!(f, "No address found for {}", addr),
        }
    }
}

/// Newtype for an address that is parsed as `String`
///
/// The main point of this newtype is to provide better description than what `String` type
/// provides.
#[derive(Deserialize)]
pub struct ResolvAddr(String);

impl ::configure_me::parse_arg::ParseArg for ResolvAddr {
    type Error = InvalidUtf8;

    fn parse_arg(arg: &OsStr) -> std::result::Result<Self, Self::Error> {
        Self::parse_owned_arg(arg.to_owned())
    }

    fn parse_owned_arg(arg: OsString) -> std::result::Result<Self, Self::Error> {
        arg.into_string().map_err(InvalidUtf8).map(ResolvAddr)
    }

    fn describe_type<W: fmt::Write>(mut writer: W) -> fmt::Result {
        write!(writer, "a network address (will be resolved if needed)")
    }
}

impl ResolvAddr {
    /// Resolves the address.
    fn resolve(self) -> std::result::Result<SocketAddr, AddressError> {
        match self.0.to_socket_addrs() {
            Ok(mut iter) => iter.next().ok_or(AddressError::NoAddrError(self.0)),
            Err(err) => Err(AddressError::ResolvError { addr: self.0, err }),
        }
    }

    /// Resolves the address, but prints error and exits in case of failure.
    fn resolve_or_exit(self) -> SocketAddr {
        self.resolve().unwrap_or_else(|err| {
            eprintln!("Error: {}", err);
            std::process::exit(1)
        })
    }
}

/// This newtype implements `ParseArg` for `Network`.
#[derive(Deserialize)]
pub struct BitcoinNetwork(Network);

impl Default for BitcoinNetwork {
    fn default() -> Self {
        BitcoinNetwork(Network::Bitcoin)
    }
}

impl FromStr for BitcoinNetwork {
    type Err = <Network as FromStr>::Err;

    fn from_str(string: &str) -> std::result::Result<Self, Self::Err> {
        Network::from_str(string).map(BitcoinNetwork)
    }
}

impl ::configure_me::parse_arg::ParseArgFromStr for BitcoinNetwork {
    fn describe_type<W: fmt::Write>(mut writer: W) -> std::fmt::Result {
        write!(writer, "either 'bitcoin', 'testnet' or 'regtest'")
    }
}

impl From<BitcoinNetwork> for Network {
    fn from(s: BitcoinNetwork) -> Network {
        s.0
    }
}

/// Parsed and post-processed configuration
pub struct Config {
    // See below for the documentation of each field:
    pub announce: bool,
    pub announce_hostname: Option<String>,
    pub announce_ssl_port: Option<u16>,
    pub announce_wss_port: Option<u16>,
    pub log: stderrlog::StdErrLog,
    pub network_type: Network,
    pub db_path: PathBuf,
    pub db_write_cache_entries: usize,
    pub daemon_dir: PathBuf,
    pub daemon_rpc_addr: SocketAddr,
    pub daemon_rpc_connections: u32,
    pub electrum_rpc_addr: SocketAddr,
    pub electrum_ws_addr: SocketAddr,
    pub monitoring_addr: SocketAddr,
    pub wait_duration: Duration,
    pub index_batch_size: usize,
    pub tx_cache_size: usize,
    pub server_banner: String,
    pub blocktxids_cache_size: usize,
    pub cookie_getter: Arc<dyn CookieGetter>,
    pub rpc_timeout: u16,
    pub low_memory: bool,
    pub cashaccount_activation_height: u32,
    pub rpc_buffer_size: usize,
    pub scripthash_subscription_limit: u32,
    pub scripthash_alias_bytes_limit: u32,
    pub rpc_max_connections: u32,
    pub rpc_max_connections_shared_prefix: u32,
    pub donation_address: Option<String>,
    pub reindex: bool,
    pub reindex_last_blocks: usize,
}

#[cfg(not(feature = "nexa"))]
fn default_daemon() -> &'static str {
    "bitcoin"
}

#[cfg(feature = "nexa")]
fn default_daemon() -> &'static str {
    "nexa"
}

#[cfg(any(target_os = "macos", target_os = "ios"))]
fn default_daemon_dir() -> PathBuf {
    // On OS X, the default directory for bitcoind/nexad is in
    // the "Application Support" folder.
    use dirs_next::data_dir;
    let mut app_support = data_dir().unwrap_or_else(|| {
        eprintln!("Error: unknown app support directory for MacOS");
        std::process::exit(1);
    });
    app_support.push(default_daemon());
    app_support
}

#[cfg(not(any(target_os = "macos", target_os = "ios")))]
fn default_daemon_dir() -> PathBuf {
    let mut home = home_dir().unwrap_or_else(|| {
        eprintln!("Error: unknown home directory");
        std::process::exit(1)
    });
    home.push(format!(".{}", default_daemon()));
    home
}

fn create_cookie_getter(
    cookie: Option<String>,
    cookie_file: Option<PathBuf>,
    daemon_dir: &Path,
) -> Arc<dyn CookieGetter> {
    match (cookie, cookie_file) {
        (None, None) => Arc::new(CookieFile::from_daemon_dir(daemon_dir)),
        (None, Some(file)) => Arc::new(CookieFile::from_file(file)),
        (Some(cookie), None) => Arc::new(StaticCookie::from_string(cookie)),
        (Some(_), Some(_)) => {
            eprintln!("Error: ambigous configuration - cookie and cookie_file can't be specified at the same time");
            std::process::exit(1);
        }
    }
}

/// Processes deprecation of cookie in favor of auth
fn select_auth(auth: Option<String>, cookie: Option<String>) -> Option<String> {
    match (cookie, auth) {
        (None, None) => None,
        (Some(value), None) => {
            eprintln!("WARNING: cookie option is deprecated and will be removed in the future!");
            eprintln!();
            eprintln!("You most likely want to use cookie_file instead.");
            eprintln!("If you really don't want to use cookie_file for a good reason and knowing the consequences use the auth option");
            eprintln!(
                "See authentication section in electrs usage documentation for more details."
            );
            eprintln!("https://github.com/romanz/electrs/blob/master/doc/usage.md#configuration-files-and-priorities");
            Some(value)
        }
        (None, Some(value)) => Some(value),
        (Some(_), Some(_)) => {
            eprintln!("Error: cookie and auth can't be specified at the same time");
            eprintln!("It looks like you made a mistake during migrating cookie option, please check your config.");
            std::process::exit(1);
        }
    }
}

#[cfg(not(feature = "nexa"))]
fn default_daemon_port(network: &Network) -> u16 {
    match network {
        Network::Bitcoin => 8332,
        Network::Testnet => 18332,
        Network::Regtest => 18443,
        Network::Testnet4 => 28332,
        Network::Scalenet => 38332,
        Network::Chipnet => 48332,
    }
}
#[cfg(feature = "nexa")]
fn default_daemon_port(network: &Network) -> u16 {
    match network {
        Network::Bitcoin => 7227,
        Network::Testnet => 7229,
        Network::Regtest => 18332,
        _ => panic!("Network {} not supported", network),
    }
}

#[cfg(not(feature = "nexa"))]
pub fn default_electrum_port(network: &Network) -> Option<u16> {
    Some(match network {
        Network::Bitcoin => 50001,
        Network::Testnet => 60001,
        Network::Regtest => 60401,
        Network::Testnet4 => 62001,
        Network::Scalenet => 63001,
        Network::Chipnet => 64001,
    })
}

#[cfg(feature = "nexa")]
pub fn default_electrum_port(network: &Network) -> Option<u16> {
    match network {
        Network::Bitcoin => Some(20001),
        Network::Testnet => Some(30001),
        Network::Regtest => Some(30401),
        _ => None,
    }
}

#[cfg(not(feature = "nexa"))]
fn default_monitoring_port(network: &Network) -> u16 {
    match network {
        Network::Bitcoin => 4224,
        Network::Testnet => 14224,
        Network::Regtest => 24224,
        Network::Testnet4 => 34224,
        Network::Scalenet => 44224,
        Network::Chipnet => 54224,
    }
}
#[cfg(feature = "nexa")]
fn default_monitoring_port(network: &Network) -> u16 {
    match network {
        Network::Bitcoin => 3224,
        Network::Testnet => 13224,
        Network::Regtest => 23224,
        _ => panic!("Network {} not supported", network),
    }
}

#[cfg(not(feature = "nexa"))]
fn default_ws_port(network: &Network) -> u16 {
    match network {
        Network::Bitcoin => 50003,
        Network::Testnet => 60003,
        Network::Regtest => 60403,
        Network::Testnet4 => 62003,
        Network::Scalenet => 63003,
        Network::Chipnet => 64003,
    }
}

#[cfg(feature = "nexa")]
fn default_ws_port(network: &Network) -> u16 {
    match network {
        Network::Bitcoin => 20003,
        Network::Testnet => 30003,
        Network::Regtest => 30403,
        _ => panic!("Network {} not supported", network),
    }
}

#[cfg(not(feature = "nexa"))]
fn get_testnet_dir(network: &Network) -> Option<String> {
    match network {
        Network::Bitcoin => None,
        Network::Testnet => Some("testnet3".to_string()),
        Network::Regtest => Some("regtest".to_string()),
        Network::Testnet4 => Some("testnet4".to_string()),
        Network::Scalenet => Some("scalenet".to_string()),
        Network::Chipnet => Some("chipnet".to_string()),
    }
}

#[cfg(feature = "nexa")]
fn get_testnet_dir(network: &Network) -> Option<String> {
    match network {
        Network::Bitcoin => None,
        Network::Testnet => Some("testnet".to_string()),
        Network::Regtest => Some("regtest".to_string()),
        _ => panic!("Network {} not supported", network),
    }
}

impl Config {
    /// Parses args, env vars, config files and post-processes them
    pub fn from_args() -> Config {
        use internal::ResultExt;

        let system_config: &OsStr = "/etc/rostrum/config.toml".as_ref();
        let home_config = home_dir().map(|mut dir| {
            dir.extend([".rostrum", "config.toml"]);
            dir
        });
        let cwd_config: &OsStr = "rostrum.toml".as_ref();
        let configs = std::iter::once(cwd_config)
            .chain(home_config.as_ref().map(AsRef::as_ref))
            .chain(std::iter::once(system_config));

        let (mut config, _) =
            internal::Config::including_optional_config_files(configs).unwrap_or_exit();

        if config.version {
            eprintln!("rostrum {} built for {}", ROSTRUM_VERSION, default_daemon());
            std::process::exit(1);
        }

        let db_subdir = match config.network {
            // We must keep the name "mainnet" due to backwards compatibility
            Network::Bitcoin => "mainnet",
            Network::Testnet => "testnet",
            Network::Regtest => "regtest",
            Network::Testnet4 => "testnet4",
            Network::Scalenet => "scalenet",
            Network::Chipnet => "chipnet",
        };

        config.db_dir.push(db_subdir);

        let daemon_rpc_addr: SocketAddr = config.daemon_rpc_addr.map_or(
            (DEFAULT_SERVER_ADDRESS, default_daemon_port(&config.network)).into(),
            ResolvAddr::resolve_or_exit,
        );
        let electrum_rpc_addr: SocketAddr = config.electrum_rpc_addr.map_or(
            (
                DEFAULT_BIND_ADDRESS,
                default_electrum_port(&config.network)
                    .expect("Network is not supported on this chain"),
            )
                .into(),
            ResolvAddr::resolve_or_exit,
        );
        let electrum_ws_addr: SocketAddr = config.electrum_ws_addr.map_or(
            (DEFAULT_BIND_ADDRESS, default_ws_port(&config.network)).into(),
            ResolvAddr::resolve_or_exit,
        );
        let monitoring_addr: SocketAddr = config.monitoring_addr.map_or(
            (
                MONITOR_BIND_ADDRESS,
                default_monitoring_port(&config.network),
            )
                .into(),
            ResolvAddr::resolve_or_exit,
        );

        if let Some(testnet_dir) = get_testnet_dir(&config.network) {
            config.daemon_dir.push(testnet_dir)
        }

        let daemon_dir = &config.daemon_dir;

        let auth = select_auth(config.auth, config.cookie);
        let cookie_getter = create_cookie_getter(auth, config.cookie_file, daemon_dir);

        let mut log = stderrlog::new();
        if config.verbose == 0 {
            // Minimum verbosity to INFO
            config.verbose = 2;
        }
        log.verbosity::<usize>(
            config
                .verbose
                .try_into()
                .expect("Overflow: Running rostrum on less than 32 bit devices is unsupported"),
        );
        log.timestamp(if config.timestamp {
            stderrlog::Timestamp::Millisecond
        } else {
            stderrlog::Timestamp::Off
        });
        log.init().unwrap_or_else(|err| {
            eprintln!("Error: logging initialization failed: {}", err);
            std::process::exit(1)
        });
        if let Some(ref a) = config.donation_address {
            decode_address(a).unwrap_or_else(|e| {
                eprintln!("Invalid donation address: {:#?}", e);
                std::process::exit(1);
            });
        };
        if config.announce {
            // Disable peer discovery if rpc is not bound to a globally routable IP.
            config.announce = ip_rfc::global(&electrum_rpc_addr.ip())
                || match electrum_rpc_addr.ip() {
                    std::net::IpAddr::V4(ip) => ip.octets() == DEFAULT_BIND_ADDRESS,
                    _ => false,
                }
        }

        const MB: f32 = (1 << 20) as f32;
        let config = Config {
            log,
            network_type: config.network,
            db_path: config.db_dir,
            db_write_cache_entries: config.db_write_cache_entries,
            daemon_dir: config.daemon_dir,
            daemon_rpc_addr,
            daemon_rpc_connections: config.daemon_rpc_connections,
            electrum_rpc_addr,
            electrum_ws_addr,
            monitoring_addr,
            wait_duration: Duration::from_secs(config.wait_duration_secs),
            index_batch_size: config.index_batch_size,
            tx_cache_size: (config.tx_cache_size_mb * MB) as usize,
            blocktxids_cache_size: (config.blocktxids_cache_size_mb * MB) as usize,
            server_banner: config.server_banner,
            cookie_getter,
            rpc_timeout: config.rpc_timeout as u16,
            low_memory: config.low_memory,
            cashaccount_activation_height: config.cashaccount_activation_height as u32,
            rpc_buffer_size: config.rpc_buffer_size,
            scripthash_subscription_limit: config.scripthash_subscription_limit,
            scripthash_alias_bytes_limit: config.scripthash_alias_bytes_limit,
            rpc_max_connections: config.rpc_max_connections,
            rpc_max_connections_shared_prefix: config.rpc_max_connections_shared_prefix,
            donation_address: config.donation_address,
            reindex: config.reindex,
            reindex_last_blocks: config.reindex_last_blocks,
            announce: config.announce,
            announce_hostname: if config.announce_hostname.is_empty() {
                None
            } else {
                Some(config.announce_hostname)
            },
            announce_ssl_port: if config.announce_ssl_port == 0 {
                None
            } else {
                Some(config.announce_ssl_port)
            },
            announce_wss_port: if config.announce_wss_port == 0 {
                None
            } else {
                Some(config.announce_wss_port)
            },
        };
        #[cfg(feature = "nexa")]
        eprintln!("Running rostrum for Nexa");

        eprintln!("{:?}", config);
        config
    }

    pub fn cookie_getter(&self) -> Arc<dyn CookieGetter> {
        Arc::clone(&self.cookie_getter)
    }
}

// CookieGetter + Debug isn't implemented in Rust, so we have to skip cookie_getter
macro_rules! debug_struct {
    ($name:ty, $($field:ident,)*) => {
        impl fmt::Debug for $name {
            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
                f.debug_struct(stringify!($name))
                    $(
                        .field(stringify!($field), &self.$field)
                    )*
                    .finish()
            }
        }
    }
}

debug_struct! { Config,
    log,
    network_type,
    db_path,
    db_write_cache_entries,
    daemon_dir,
    daemon_rpc_addr,
    daemon_rpc_connections,
    electrum_rpc_addr,
    electrum_ws_addr,
    monitoring_addr,
    index_batch_size,
    tx_cache_size,
    server_banner,
    blocktxids_cache_size,
    rpc_timeout,
    low_memory,
    cashaccount_activation_height,
    rpc_buffer_size,
    scripthash_subscription_limit,
    scripthash_alias_bytes_limit,
    rpc_max_connections,
    rpc_max_connections_shared_prefix,
}

struct StaticCookie {
    value: Vec<u8>,
}

impl StaticCookie {
    fn from_string(value: String) -> Self {
        StaticCookie {
            value: value.into(),
        }
    }
}

impl CookieGetter for StaticCookie {
    fn get(&self) -> Result<Vec<u8>> {
        Ok(self.value.clone())
    }
}

struct CookieFile {
    cookie_file: PathBuf,
}

impl CookieFile {
    fn from_daemon_dir(daemon_dir: &Path) -> Self {
        CookieFile {
            cookie_file: daemon_dir.join(".cookie"),
        }
    }

    fn from_file(cookie_file: PathBuf) -> Self {
        CookieFile { cookie_file }
    }
}

impl CookieGetter for CookieFile {
    fn get(&self) -> Result<Vec<u8>> {
        let contents = fs::read(&self.cookie_file).context(ConnectionError {
            msg: format!("failed to read cookie from {}", self.cookie_file.display()),
        })?;
        Ok(contents)
    }
}