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};
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"));
}
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)
}
}
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),
}
}
}
#[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 {
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 }),
}
}
fn resolve_or_exit(self) -> SocketAddr {
self.resolve().unwrap_or_else(|err| {
eprintln!("Error: {}", err);
std::process::exit(1)
})
}
}
#[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
}
}
pub struct Config {
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 {
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);
}
}
}
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 {
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 {
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 {
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 {
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)
}
}
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)
}
}